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

zopefoundation / zc.form / 16248976557

21 Oct 2024 07:16AM UTC coverage: 82.759%. Remained the same
16248976557

push

github

icemac
Back to development: 2.2

126 of 180 branches covered (70.0%)

Branch coverage included in aggregate %.

786 of 922 relevant lines covered (85.25%)

0.85 hits per line

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

82.25
/src/zc/form/field.py
1
##############################################################################
2
#
3
# Copyright (c) 2003-2004 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
"""fields
15

16
$Id: field.py 4634 2006-01-06 20:21:15Z fred $
17
"""
18
import zope.catalog.interfaces
1✔
19
import zope.index.text.parsetree
1✔
20
import zope.index.text.queryparser
1✔
21
from zope import component
1✔
22
from zope import i18n
1✔
23
from zope import interface
1✔
24
from zope import schema
1✔
25
from zope.interface.exceptions import DoesNotImplement
1✔
26
from zope.schema.interfaces import IField
1✔
27
from zope.schema.interfaces import ValidationError
1✔
28
from zope.schema.interfaces import WrongType
1✔
29

30
from zc.form import interfaces
1✔
31
from zc.form.i18n import _
1✔
32

33

34
_no_unioned_field_validates = _(
1✔
35
    "No unioned field validates ${value}.")
36

37
_range_less_error = _("${minimum} must be less than ${maximum}.")
1✔
38
_range_less_equal_error = _(
1✔
39
    "${minimum} must be less than or equal to ${maximum}.")
40
_combination_wrong_size_error = _("The value has the wrong number of members")
1✔
41
_combination_not_a_sequence_error = _("The value is not a sequence")
1✔
42
_bad_query = _("Invalid query.")
1✔
43

44
# Union field that accepts other fields...
45

46

47
class MessageValidationError(ValidationError):
1✔
48
    """ValidationError that takes a message"""
49

50
    def __init__(self, message, mapping=None):
1✔
51
        if mapping is not None:
1✔
52
            self.message = i18n.Message(message, mapping=mapping)
1✔
53
        else:
54
            self.message = message
1✔
55
        self.args = (message, mapping)
1✔
56

57
    def doc(self):
1✔
58
        return self.message
1✔
59

60

61
@interface.implementer(interfaces.IExtendedField)
1✔
62
class BaseField(schema.Field):
1✔
63
    """Field with a callable as default and a tuple of constraints.
64

65
    >>> def secure_password(field, value):
66
    ...     if len(value) < 8:
67
    ...         raise schema.ValidationError('Password too short.')
68
    ...
69
    >>> class IDummy(interface.Interface):
70
    ...     suggested_password = BaseField(
71
    ...         title=u'Suggested Password',
72
    ...         default_getter=lambda context: u'asdf',
73
    ...         constraints=(secure_password, ))
74
    ...
75
    >>> f = IDummy['suggested_password'].bind(None) # use None as context
76
    >>> interfaces.IExtendedField.providedBy(f)
77
    True
78
    >>> f.__name__
79
    'suggested_password'
80
    >>> print(f.title)
81
    Suggested Password
82
    >>> print(f.default)
83
    asdf
84
    >>> f.validate(u'123456789')
85
    >>> f.validate(u'asdf')
86
    Traceback (most recent call last):
87
    ...
88
    ValidationError: Password too short.
89
    >>> class IDummy2(interface.Interface):
90
    ...     invalid_default = BaseField(
91
    ...         title=u'Field with invalid default',
92
    ...         default=u'standard',
93
    ...         default_getter=lambda context: u'get default')
94
    Traceback (most recent call last):
95
    ...
96
    TypeError: may not specify both a default and a default_getter
97
    """
98
    constraints = ()
1✔
99
    _default = default_getter = None
1✔
100

101
    def __init__(self, constraints=(), default_getter=None, **kw):
1✔
102
        self.constraints = constraints
1✔
103
        if default_getter is not None and 'default' in kw:
1✔
104
            raise TypeError(
1✔
105
                'may not specify both a default and a default_getter')
106
        super().__init__(**kw)
1✔
107
        self.default_getter = default_getter
1✔
108

109
    def _validate(self, value):
1✔
110
        super()._validate(value)
1✔
111
        if value != self.missing_value:
1!
112
            for constraint in self.constraints:
1✔
113
                constraint(self, value)
1✔
114

115
    @property
1✔
116
    def default(self):
1✔
117
        if self.default_getter is not None:
1✔
118
            return self.default_getter(self.context)
1✔
119
        else:
120
            return self._default
1✔
121

122
    @default.setter
1✔
123
    def default(self, value):
1✔
124
        assert self.default_getter is None
1✔
125
        self._default = value
1✔
126

127

128
@interface.implementer(interfaces.IOptionField)
1✔
129
class Option(BaseField):
1✔
130
    """A field with one predefined value."""
131

132
    def __init__(self, value=None, value_getter=None,
1✔
133
                 identity_comparison=False, **kw):
134
        self.value = value
1✔
135
        self.value_getter = value_getter
1✔
136
        self.identity_comparison = identity_comparison
1✔
137
        assert (value is None) ^ (value_getter is None)
1✔
138
        assert not kw.get('required')
1✔
139
        kw['required'] = False
1✔
140
        super().__init__(**kw)
1✔
141

142
    def _validate(self, value):
1✔
143
        if value != self.missing_value:
×
144
            if self.identity_comparison:
×
145
                if self.getValue() is not value:
×
146
                    raise WrongType
×
147
            elif self.getValue() != value:
×
148
                raise WrongType
×
149

150
    def getValue(self):
1✔
151
        if self.value_getter is not None:
×
152
            return self.value_getter(self.context)
×
153
        else:
154
            return self.value
×
155

156

157
@interface.implementer(interfaces.IUnionField)
1✔
158
class Union(BaseField):
1✔
159
    """Union field allows a schema field to hold one of many other field types.
160

161
    For instance, you might want to make a field that can hold
162
    a duration *or* a date, if you are working with a PIM app.  Or perhaps
163
    you want to have a field that can hold a string from a vocabulary *or* a
164
    custom string.  Both of these examples can be accomplished in a variety of
165
    ways--the union field is one option.
166

167
    The second example is more easily illustrated.  Here is a union field that
168
    is a simple way of allowing "write-ins" in addition to selections from a
169
    choice.  We'll be very explicit about imports, in part so this can be
170
    trivially moved to a doc file test.
171

172
    Notice as you look through the example that field order does matter: the
173
    first field as entered in the field list that validates is chosen as the
174
    validField; thus, even though the options in the Choice field would also
175
    validate in a TextLine, the Choice field is identified as the "validField"
176
    because it is first.
177

178
    >>> class IDummy(interface.Interface):
179
    ...     cartoon_character = Union((
180
    ...         schema.Choice(
181
    ...             title=u'Disney',
182
    ...             description=u'Some tried-and-true Disney characters',
183
    ...             values=(u'Goofy',u'Mickey',u'Donald',u'Minnie')),
184
    ...         schema.TextLine(
185
    ...             title=u'Write-in',
186
    ...             description=u'Name your own!')),
187
    ...         required=True,
188
    ...         title=u'Cartoon Character',
189
    ...         description=u'Your favorite cartoon character')
190
    ...
191
    >>> f = IDummy['cartoon_character']
192
    >>> interfaces.IUnionField.providedBy(f)
193
    True
194
    >>> f.__name__
195
    'cartoon_character'
196
    >>> isinstance(f.fields[0], schema.Choice)
197
    True
198
    >>> isinstance(f.fields[1], schema.TextLine)
199
    True
200
    >>> f.fields[0].__name__ != f.fields[1].__name__
201
    True
202
    >>> len(f.fields)
203
    2
204
    >>> print(f.title)
205
    Cartoon Character
206
    >>> f.validate(u'Goofy')
207
    >>> f.validField(u'Goofy') is f.fields[0]
208
    True
209
    >>> f.validate(u'Calvin')
210
    >>> f.validField(u'Calvin') is f.fields[1]
211
    True
212
    >>> f.validate(42)
213
    Traceback (most recent call last):
214
    ...
215
    MessageValidationError: (u'No unioned field validates ${value}.', {'value': 42})
216

217
    That's a working example.  Now lets close with a couple of examples that
218
    should fall over.
219

220
    You must union at least two fields:
221

222
    >>> f = Union((schema.TextLine(title=u'Foo Text Line!'),), title=u'Foo')
223
    Traceback (most recent call last):
224
    ...
225
    ValueError: union must combine two or more fields
226

227
    And, unsurprisingly, they must actually be fields:
228

229
    >>> from zope.interface.exceptions import DoesNotImplement
230
    >>> try:
231
    ...     f = Union(('I am not a number.', 'I am a free man!'), title=u'Bar')
232
    ... except DoesNotImplement:
233
    ...     print("Not a field")
234
    ...
235
    Not a field
236

237
    Binding a union field also takes care of binding the contained fields:
238

239
    >>> context = object()
240
    >>> bound_f = f.bind(context)
241
    >>> bound_f.context is context
242
    True
243
    >>> bound_f.fields[0].context is context
244
    True
245
    >>> bound_f.fields[1].context is context
246
    True
247
    """  # noqa
248
    fields = ()
1✔
249
    use_default_for_not_selected = False
1✔
250

251
    def __init__(self, fields, use_default_for_not_selected=False, **kw):
1✔
252
        if len(fields) < 2:
1✔
253
            raise ValueError(_("union must combine two or more fields"))
1✔
254
        for ix, field in enumerate(fields):
1✔
255
            if not IField.providedBy(field):
1✔
256
                raise DoesNotImplement(IField)
1✔
257
            field.__name__ = "unioned_%02d" % ix
1✔
258
        self.fields = tuple(fields)
1✔
259
        self.use_default_for_not_selected = use_default_for_not_selected
1✔
260
        super().__init__(**kw)
1✔
261

262
    def bind(self, object):
1✔
263
        clone = super().bind(object)
1✔
264
        # We need to bind the fields too, e.g. for Choice fields
265
        clone.fields = tuple(field.bind(object) for field in clone.fields)
1✔
266
        return clone
1✔
267

268
    def validField(self, value):
1✔
269
        """Return first valid field, or None."""
270
        for field in self.fields:
1✔
271
            try:
1✔
272
                field.validate(value)
1✔
273
            except ValidationError:
1✔
274
                pass
1✔
275
            else:
276
                return field
1✔
277

278
    def _validate(self, value):
1✔
279
        if self.validField(value) is None:
1✔
280
            raise MessageValidationError(_no_unioned_field_validates,
1✔
281
                                         {'value': value})
282

283

284
class OrderedCombinationConstraint:
1✔
285

286
    def __init__(self, may_be_equal=True, decreasing=False):
1✔
287
        self.may_be_equal = may_be_equal
1✔
288
        self.decreasing = decreasing
1✔
289

290
    def __call__(self, field, value):
1✔
291
        # can assume that len(value) == len(field.fields)
292
        last = None
1✔
293
        for v, f in zip(value, field.fields):
1✔
294
            if v != f.missing_value:
1✔
295
                if last is not None:
1✔
296
                    if self.decreasing:
1!
297
                        if self.may_be_equal:
×
298
                            if v > last:
×
299
                                raise MessageValidationError(
×
300
                                    _range_less_equal_error,
301
                                    {'minimum': v, 'maximum': last})
302
                        elif v >= last:
×
303
                            raise MessageValidationError(
×
304
                                _range_less_error,
305
                                {'minimum': v, 'maximum': last})
306
                    else:
307
                        if self.may_be_equal:
1!
308
                            if v < last:
1✔
309
                                raise MessageValidationError(
1✔
310
                                    _range_less_equal_error,
311
                                    {'minimum': last, 'maximum': v})
312
                        elif v <= last:
×
313
                            raise MessageValidationError(
×
314
                                _range_less_error,
315
                                {'minimum': last, 'maximum': v})
316
                last = v
1✔
317

318

319
@interface.implementer(interfaces.ICombinationField)
1✔
320
class Combination(BaseField):
1✔
321
    """a combination of two or more fields, all of which may be completed.
322

323
    It accepts two or more fields.  It also accepts a 'constraints' argument.
324
    Unlike the usual 'constraint' argument (which is also available), the
325
    constraints should be a sequence of callables that take a field and a
326
    value, and they should raise an error if there is a problem.
327

328
    >>> from zc.form.field import Combination, OrderedCombinationConstraint
329
    >>> from zope import schema, interface
330
    >>> class IDemo(interface.Interface):
331
    ...     publication_range = Combination(
332
    ...         (schema.Date(title=u'Begin', required=False),
333
    ...          schema.Date(title=u'Expire', required=True)),
334
    ...         title=u'Publication Range',
335
    ...         required=True,
336
    ...         constraints=(OrderedCombinationConstraint(),))
337
    ...
338
    >>> f = IDemo['publication_range']
339
    >>> interfaces.ICombinationField.providedBy(f)
340
    True
341
    >>> f.__name__
342
    'publication_range'
343
    >>> isinstance(f.fields[0], schema.Date)
344
    True
345
    >>> isinstance(f.fields[1], schema.Date)
346
    True
347
    >>> print(f.title)
348
    Publication Range
349
    >>> print(f.fields[0].title)
350
    Begin
351
    >>> print(f.fields[1].title)
352
    Expire
353
    >>> import datetime
354
    >>> f.validate((datetime.date(2005, 6, 22), datetime.date(2005, 7, 10)))
355
    >>> f.validate((None, datetime.date(2005, 7, 10)))
356
    >>> f.validate((datetime.date(2005, 6, 22), None))
357
    Traceback (most recent call last):
358
    ...
359
    RequiredMissing: combination_01
360
    >>> f.validate(('foo', datetime.date(2005, 6, 22)))
361
    Traceback (most recent call last):
362
    ...
363
    WrongType: ('foo', <type 'datetime.date'>, 'combination_00')
364
    >>> f.validate('foo') # doctest: +ELLIPSIS
365
    Traceback (most recent call last):
366
    ...
367
    MessageValidationError: (u'The value has the wrong number of members', ...)
368
    >>> f.validate(17)
369
    Traceback (most recent call last):
370
    ...
371
    MessageValidationError: (u'The value is not a sequence', None)
372
    >>> f.validate((datetime.date(2005, 6, 22), datetime.date(1995, 7, 10)))
373
    ... # doctest: +ELLIPSIS
374
    Traceback (most recent call last):
375
    ...
376
    MessageValidationError: (u'${minimum} must be less than or equal to ...
377

378
    Binding a Combination field also takes care of binding contained fields:
379

380
    >>> context = object()
381
    >>> bound_f = f.bind(context)
382
    >>> bound_f.context is context
383
    True
384
    >>> bound_f.fields[0].context is context
385
    True
386
    >>> bound_f.fields[1].context is context
387
    True
388

389
    Each entry in the combination has to be a schema field
390

391
    >>> class IDemo2(interface.Interface):
392
    ...     invalid_field = Combination(
393
    ...         (schema.Date(title=u'Begin', required=False),
394
    ...          dict(title=u'Expire', required=True)),
395
    ...         title=u'Invalid field')
396
    Traceback (most recent call last):
397
    ...
398
    DoesNotImplement: An object does not implement interface...
399
    """
400
    fields = constraints = ()
1✔
401

402
    def __init__(self, fields, **kw):
1✔
403
        for ix, field in enumerate(fields):
1✔
404
            if not IField.providedBy(field):
1✔
405
                raise DoesNotImplement(IField)
1✔
406
            field.__name__ = "combination_%02d" % ix
1✔
407
        self.fields = tuple(fields)
1✔
408
        super().__init__(**kw)
1✔
409

410
    def _validate(self, value):
1✔
411
        if value != self.missing_value:
1!
412
            try:
1✔
413
                len_value = len(value)
1✔
414
            except (TypeError, AttributeError):
1✔
415
                raise MessageValidationError(
1✔
416
                    _combination_not_a_sequence_error)
417
            if len_value != len(self.fields):
1✔
418
                raise MessageValidationError(
1✔
419
                    _combination_wrong_size_error)
420
            for v, f in zip(value, self.fields):
1✔
421
                f = f.bind(self.context)
1✔
422
                f.validate(v)
1✔
423
        super()._validate(value)
1✔
424

425
    def bind(self, object):
1✔
426
        clone = super().bind(object)
1✔
427
        # We need to bind the fields too, e.g. for Choice fields
428
        clone.fields = tuple(field.bind(object) for field in clone.fields)
1✔
429
        return clone
1✔
430

431

432
class QueryTextLineConstraint(BaseField, schema.TextLine):
1✔
433
    def __init__(self, index_getter=None, catalog_name=None, index_name=None):
1✔
434
        assert not ((catalog_name is None) ^ (index_name is None))
1✔
435
        assert (index_getter is None) ^ (catalog_name is None)
1✔
436
        self.catalog_name = catalog_name
1✔
437
        self.index_name = index_name
1✔
438
        self.index_getter = index_getter
1✔
439

440
    def __call__(self, field, value):
1✔
441
        if self.index_getter is not None:
1!
442
            index = self.index_getter(self.context)
1✔
443
        else:
444
            catalog = component.getUtility(
×
445
                zope.catalog.interfaces.ICatalog,
446
                self.catalog_name,
447
                field.context)
448
            index = catalog[self.index_name]
×
449
        parser = zope.index.text.queryparser.QueryParser(index.lexicon)
1✔
450
        try:
1✔
451
            parser.parseQuery(value)
1✔
452
        except zope.index.text.parsetree.ParseError:
1✔
453
            raise MessageValidationError(_bad_query)
1✔
454

455

456
class TextLine(BaseField, schema.TextLine):
1✔
457
    """An extended TextLine.
458

459
    >>> from zope.index.text.textindex import TextIndex
460
    >>> index = TextIndex()
461
    >>> class IDemo(interface.Interface):
462
    ...     query = TextLine(
463
    ...         constraints=(
464
    ...             QueryTextLineConstraint(
465
    ...                 lambda context: index),),
466
    ...         title=u"Text Query")
467
    ...
468
    >>> field = IDemo['query'].bind(None) # using None as context
469
    >>> field.validate(u'cow')
470
    >>> field.validate(u'cow and dog')
471
    >>> field.validate(u'a the') # doctest: +ELLIPSIS
472
    Traceback (most recent call last):
473
    ...
474
    MessageValidationError: (u'Invalid query.', None)
475
    >>> field.validate(u'and') # doctest: +ELLIPSIS
476
    Traceback (most recent call last):
477
    ...
478
    MessageValidationError: (u'Invalid query.', None)
479
    >>> field.validate(u'cow not dog')
480
    >>> field.validate(u'cow not not dog') # doctest: +ELLIPSIS
481
    Traceback (most recent call last):
482
    ...
483
    MessageValidationError: (u'Invalid query.', None)
484
    >>> field.validate(u'cow and not dog')
485
    >>> field.validate(u'cow -dog')
486
    >>> field.validate(b'cow') # non-unicode fails, as usual with TextLine
487
    Traceback (most recent call last):
488
    ...
489
    WrongType: ('cow', <type 'unicode'>, 'query')
490
    """
491

492

493
@interface.implementer(interfaces.IHTMLSnippet)
1✔
494
class HTMLSnippet(BaseField, schema.Text):
1✔
495
    """Simple implementation for HTML snippet."""
496

497

498
@interface.implementer(interfaces.IHTMLDocument)
1✔
499
class HTMLDocument(BaseField, schema.Text):
1✔
500
    """Simple implementation for HTML document."""
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