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

hasgeek / baseframe / 16218997531

11 Jul 2025 11:32AM UTC coverage: 67.889% (+1.0%) from 66.866%
16218997531

push

github

web-flow
Fix linter issues; fix ValidCoordinates; add OptionalCoordinates (#511)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

120 of 149 new or added lines in 14 files covered. (80.54%)

3 existing lines in 2 files now uncovered.

1592 of 2345 relevant lines covered (67.89%)

2.72 hits per line

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

61.27
/src/baseframe/forms/form.py
1
"""Form base class and redefined fields with ParsleyJS support."""
2

3
from __future__ import annotations
4✔
4

5
import warnings
4✔
6
from collections.abc import Iterable
4✔
7
from typing import Any, Callable, Optional, Union
4✔
8
from typing_extensions import TypeAlias
4✔
9

10
import wtforms
4✔
11
from flask import current_app
4✔
12
from flask_wtf import FlaskForm as BaseForm
4✔
13
from werkzeug.datastructures import MultiDict
4✔
14
from wtforms import Field as WTField
4✔
15
from wtforms.utils import unset_value
4✔
16

17
from ..signals import form_validation_error, form_validation_success
4✔
18
from . import (
4✔
19
    fields as bfields,
20
    filters as bfilters,
21
    parsleyjs as bparsleyjs,
22
    validators as bvalidators,
23
)
24
from .typing import FilterCallable, ValidatorCallable, ValidatorList, WidgetProtocol
4✔
25

26
__all__ = [
4✔
27
    'Form',
28
    'FormGenerator',
29
    'RecaptchaForm',
30
    'field_registry',
31
    'validator_registry',
32
    'widget_registry',
33
]
34

35
# Use a hardcoded list to control what is available to user-facing apps
36
field_registry: dict[str, WTField] = {
4✔
37
    'SelectField': bparsleyjs.SelectField,
38
    'SelectMultipleField': bfields.SelectMultipleField,
39
    'RadioField': bparsleyjs.RadioField,
40
    'StringField': bparsleyjs.StringField,
41
    'IntegerField': bparsleyjs.IntegerField,
42
    'DecimalField': bparsleyjs.DecimalField,
43
    'FloatField': bparsleyjs.FloatField,
44
    'BooleanField': bparsleyjs.BooleanField,
45
    'TelField': bparsleyjs.TelField,
46
    'URLField': bparsleyjs.URLField,
47
    'EmailField': bparsleyjs.EmailField,
48
    'DateTimeField': bfields.DateTimeField,
49
    'DateField': bparsleyjs.DateField,
50
    'TextAreaField': bparsleyjs.TextAreaField,
51
    'PasswordField': bparsleyjs.PasswordField,
52
    # Baseframe fields
53
    'RichTextField': bfields.TinyMce4Field,
54
    'TextListField': bfields.TextListField,
55
    'UserSelectField': bfields.UserSelectField,
56
    'UserSelectMultiField': bfields.UserSelectMultiField,
57
    'GeonameSelectField': bfields.GeonameSelectField,
58
    'GeonameSelectMultiField': bfields.GeonameSelectMultiField,
59
    'AnnotatedTextField': bfields.AnnotatedTextField,
60
    'MarkdownField': bfields.MarkdownField,
61
    'ImageField': bfields.ImgeeField,
62
}
63

64
WidgetRegistryEntry: TypeAlias = tuple[Callable[..., WidgetProtocol]]
4✔
65
widget_registry: dict[str, WidgetRegistryEntry] = {}
4✔
66

67
ValidatorRegistryEntry: TypeAlias = Union[
4✔
68
    tuple[Callable[..., ValidatorCallable]],
69
    tuple[Callable[..., ValidatorCallable], str],
70
    tuple[Callable[..., ValidatorCallable], str, str],
71
    tuple[Callable[..., ValidatorCallable], str, str, str],
72
]
73
validator_registry: dict[str, ValidatorRegistryEntry] = {
4✔
74
    'Length': (wtforms.validators.Length, 'min', 'max', 'message'),
75
    'NumberRange': (wtforms.validators.NumberRange, 'min', 'max', 'message'),
76
    'Optional': (wtforms.validators.Optional, 'strip_whitespace'),
77
    'Required': (wtforms.validators.DataRequired, 'message'),
78
    'AnyOf': (wtforms.validators.AnyOf, 'values', 'message'),
79
    'NoneOf': (wtforms.validators.NoneOf, 'values', 'message'),
80
    'ValidEmail': (bvalidators.ValidEmail,),
81
    'ValidUrl': (bvalidators.ValidUrl,),
82
    'AllUrlsValid': (bvalidators.AllUrlsValid,),
83
}
84

85
FilterRegistryEntry: TypeAlias = Union[
4✔
86
    tuple[Callable[..., FilterCallable]],
87
    tuple[Callable[..., FilterCallable], str],
88
]
89
filter_registry: dict[str, FilterRegistryEntry] = {
4✔
90
    'lower': (bfilters.lower,),
91
    'upper': (bfilters.upper,),
92
    'strip': (bfilters.strip, 'chars'),
93
    'lstrip': (bfilters.lstrip, 'chars'),
94
    'rstrip': (bfilters.rstrip, 'chars'),
95
    'none_if_empty': (bfilters.none_if_empty,),
96
}
97

98

99
class Form(BaseForm):
4✔
100
    """Form with additional methods."""
101

102
    __expects__: Iterable[str] = ()
4✔
103
    __returns__: Iterable[str] = ()
4✔
104

105
    def __init_subclass__(cls, **kwargs: Any) -> None:
4✔
106
        """Validate :attr:`__expects__` and :attr:`__returns__` in sub-classes."""
107
        super().__init_subclass__(**kwargs)
4✔
108
        if {'edit_obj', 'edit_model', 'edit_parent', 'edit_id'} & set(cls.__expects__):
4✔
109
            raise TypeError(
×
110
                "This form has __expects__ parameters that are reserved by the base"
111
                " form"
112
            )
113

114
        if set(cls.__dict__.keys()) & set(cls.__expects__):
4✔
115
            raise TypeError(
×
116
                "This form has __expects__ parameters that clash with field names"
117
            )
118
        if 'set_queries' in cls.__dict__ and 'queries' not in cls.__dict__:
4✔
119
            warnings.warn(
4✔
120
                f"`{cls.__qualname__}.set_queries` is deprecated due to conflict with"
121
                " `set_<fieldname>` methods. Rename it to `__post_init__`",
122
                stacklevel=2,
123
            )
124

125
    def __init__(self, *args: Any, **kwargs: Any) -> None:
4✔
126
        for attr in self.__expects__:
4✔
127
            if attr not in kwargs:
4✔
128
                raise TypeError(f"Expected parameter {attr} was not supplied")
4✔
129
            setattr(self, attr, kwargs.pop(attr))
4✔
130

131
        # TODO: These fields predate the `__expects__` protocol and are pending
132
        # deprecation.
133
        self.edit_obj = kwargs.get('obj')
4✔
134
        self.edit_model = kwargs.get('model')
4✔
135
        self.edit_parent = kwargs.get('parent')
4✔
136
        if self.edit_obj:
4✔
137
            if hasattr(self.edit_obj, 'id'):
4✔
138
                self.edit_id = self.edit_obj.id
×
139
            else:
140
                self.edit_id = None
4✔
141
            if not self.edit_model:
4✔
142
                self.edit_model = self.edit_obj.__class__
4✔
143
            if not self.edit_parent and hasattr(self.edit_obj, 'parent'):
4✔
144
                self.edit_parent = self.edit_obj.parent
×
145
        else:
146
            self.edit_id = None
4✔
147

148
        # Call baseclass after expected parameters have been set. `__init__` will call
149
        # `process`, which will in turn call the ``get_<fieldname>`` methods, and they
150
        # will need proper context
151
        super().__init__(*args, **kwargs)
4✔
152

153
        # Finally, populate the ``choices`` attr of selection fields
154
        if callable(post_init := getattr(self, '__post_init__', None)) or callable(
4✔
155
            post_init := getattr(self, 'set_queries', None)
156
        ):
157
            post_init()  # pylint: disable=not-callable
4✔
158

159
    def __json__(self) -> Any:
4✔
160
        """Render this form as JSON."""
161
        return [field.__json__() for field in self._fields.values()]
×
162

163
    def populate_obj(self, obj: Any) -> None:
4✔
164
        """
165
        Populate the attributes of the passed `obj` with data from the form's fields.
166

167
        If the form has a ``set_<fieldname>`` method, it will be called with the object
168
        in place of the field's ``populate_obj`` method. The custom method is then
169
        responsible for populating the object with that field's value.
170

171
        This method overrides the default implementation in WTForms to support custom
172
        set methods.
173
        """
174
        for name, field in self._fields.items():
4✔
175
            if hasattr(self, 'set_' + name):
4✔
176
                getattr(self, 'set_' + name)(obj)
4✔
177
            else:
178
                field.populate_obj(obj, name)
4✔
179

180
    def process(
4✔
181
        self,
182
        formdata: Optional[MultiDict] = None,
183
        obj: Any = None,
184
        data: Optional[dict[str, Any]] = None,
185
        extra_filters: Optional[dict[str, Iterable[Callable[[Any], Any]]]] = None,
186
        **kwargs: Any,
187
    ) -> None:
188
        """
189
        Take form, object data, and keyword arg input and have the fields process them.
190

191
        :param formdata: Used to pass data coming from the client, usually
192
            `request.POST` or equivalent.
193
        :param obj: If `formdata` is empty or not provided, this object is checked for
194
            attributes matching form field names, which will be used for field values.
195
            If the form has a ``get_<fieldname>`` method, it will be called with the
196
            object as an attribute and is expected to return the value
197
        :param data: If provided, must be a dictionary of data. This is only used if
198
            `formdata` is empty or not provided and `obj` does not contain an attribute
199
            named the same as the field.
200
        :param extra_filters: A dict mapping field attribute names to lists of extra
201
            filter functions to run. Extra filters run after filters passed when
202
            creating the field. If the form has ``filter_<fieldname>``, it is the last
203
            extra filter.
204
        :param kwargs: If `formdata` is empty or not provided and `obj` does not contain
205
            an attribute named the same as a field, form will assign the value of a
206
            matching keyword argument to the field, if one exists.
207

208
        This method overrides the default implementation in WTForms to support custom
209
        load methods.
210
        """
211
        formdata = self.meta.wrap_formdata(self, formdata)
4✔
212

213
        if data is not None:
4✔
214
            kwargs = dict(data, **kwargs)
×
215

216
        filters = extra_filters.copy() if extra_filters is not None else {}
4✔
217

218
        for name, field in self._fields.items():
4✔
219
            field_extra_filters = list(filters.get(name, []))
4✔
220

221
            inline_filter = getattr(self, f'filter_{name}', None)
4✔
222
            if inline_filter is not None:
4✔
223
                field_extra_filters.append(inline_filter)
×
224

225
            # This `if` condition is the only change from the WTForms source. It must be
226
            # synced with the `process` method in future WTForms releases.
227
            if obj is not None and hasattr(self, f'get_{name}'):
4✔
228
                data = getattr(self, f'get_{name}')(obj)
4✔
229
            elif obj is not None and hasattr(obj, name):
4✔
230
                data = getattr(obj, name)
4✔
231
            else:
232
                data = kwargs.get(name, unset_value)
4✔
233

234
            field.process(formdata, data, extra_filters=field_extra_filters)
4✔
235

236
    def validate(
4✔
237
        self,
238
        extra_validators: Optional[dict[str, ValidatorList]] = None,
239
        send_signals: bool = True,
240
    ) -> bool:
241
        """
242
        Validate a form.
243

244
        :param extra_validators: A dict of field name to list of extra validators
245
        :param send_signals: Raise :attr:`~baseframe.signals.form_validation_success` or
246
            :attr:`~baseframe.signals.form_validation_error` after validation
247

248
        Signal handlers may be used to record analytics. Baseframe provides default
249
        handlers that log to :class:`~baseframe.statsd.Statsd` if enabled for the app,
250
        tagging the names of erroring fields in case of errors.
251
        """
252
        success = super().validate(extra_validators)
4✔
253
        for attr in self.__returns__:
4✔
254
            if not hasattr(self, attr):
×
255
                setattr(self, attr, None)
×
256
        if send_signals:
4✔
257
            self.send_signals(success)
4✔
258
        return success
4✔
259

260
    def send_signals(self, success: Optional[bool] = None) -> None:
4✔
261
        if success is None:
4✔
262
            success = not self.errors
×
263
        if success:
4✔
264
            form_validation_success.send(self)
4✔
265
        else:
266
            form_validation_error.send(self)
4✔
267

268
    def errors_with_data(self) -> dict:
4✔
269
        return {
×
270
            name: {
271
                'data': f.data,
272
                'errors': [str(e) for e in f.errors],  # str(lazy_gettext) needed
273
            }
274
            for name, f in self._fields.items()
275
            if f.errors
276
        }
277

278

279
class FormGenerator:
4✔
280
    """
281
    Creates forms from a JSON-compatible dictionary structure.
282

283
    Consults an allowed set of fields, widgets, validators and filters.
284
    """
285

286
    def __init__(
4✔
287
        self,
288
        fields: Optional[dict[str, WTField]] = None,
289
        widgets: Optional[dict[str, WidgetRegistryEntry]] = None,
290
        validators: Optional[dict[str, ValidatorRegistryEntry]] = None,
291
        filters: Optional[dict[str, FilterRegistryEntry]] = None,
292
        default_field: str = 'StringField',
293
    ) -> None:
294
        # If using global defaults, make a copy in this class so that
295
        # they can be customised post-init without clobbering the globals
296
        self.fields = fields or dict(field_registry)
×
297
        self.widgets = widgets or dict(widget_registry)
×
298
        self.validators = validators or dict(validator_registry)
×
299
        self.filters = filters or dict(filter_registry)
×
300

301
        self.default_field = default_field
×
302

303
    # TODO: Make `formstruct` a TypedDict
304
    def generate(self, formstruct: dict) -> type[Form]:
4✔
305
        """Generate a dynamic form from the given data structure."""
306

307
        class DynamicForm(Form):
×
308
            pass
×
309

NEW
310
        for _fielddata in formstruct:
×
NEW
311
            fielddata = dict(_fielddata)  # Make a copy
×
312
            name = fielddata.pop('name', None)
×
313
            type_ = fielddata.pop('type', None)
×
314
            if not name:
×
315
                continue  # Skip unnamed fields
×
316
            if (not type_) or type_ not in field_registry:
×
317
                type_ = self.default_field  # Default to string input
×
318

319
            # TODO: Process widget requests
320

321
            # Make a list of validators
322
            validators = []
×
323
            validators_data = fielddata.pop('validators', [])
×
324
            for item in validators_data:
×
325
                if isinstance(item, str) and item in validator_registry:
×
326
                    validators.append(validator_registry[item][0]())
×
327
                else:
328
                    itemname = item.pop('type', None)
×
329
                    itemparams = {}
×
330
                    if itemname:
×
331
                        for paramname in item:
×
332
                            if paramname in validator_registry[itemname][1:]:
×
333
                                itemparams[paramname] = item[paramname]
×
334
                        validators.append(validator_registry[itemname][0](**itemparams))
×
335

336
            # Make a list of filters
337
            filters = []
×
338
            filters_data = fielddata.pop('filters', [])
×
339
            for item in filters_data:
×
340
                if isinstance(item, str) and item in filter_registry:
×
341
                    filters.append(filter_registry[item][0]())
×
342
                else:
343
                    itemname = item.pop('type', None)
×
344
                    itemparams = {}
×
345
                    if itemname:
×
346
                        for paramname in item:
×
347
                            if paramname in filter_registry[itemname][1:]:
×
348
                                itemparams[paramname] = item[paramname]
×
349
                        filters.append(filter_registry[itemname][0](**itemparams))
×
350

351
            # TODO: Also validate the parameters in `fielddata`, like with validators
352
            # above
353
            setattr(
×
354
                DynamicForm,
355
                name,
356
                field_registry[type_](
357
                    validators=validators, filters=filters, **fielddata
358
                ),
359
            )
360
        return DynamicForm
×
361

362

363
class RecaptchaForm(Form):
4✔
364
    """Base class for forms that use Recaptcha."""
365

366
    recaptcha = bfields.RecaptchaField()
4✔
367

368
    def __init__(self, *args: Any, **kwargs: Any) -> None:
4✔
369
        super().__init__(*args, **kwargs)
×
370
        if not (
×
371
            current_app.config.get('RECAPTCHA_PUBLIC_KEY')
372
            and current_app.config.get('RECAPTCHA_PRIVATE_KEY')
373
        ):
374
            del self.recaptcha
×
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

© 2026 Coveralls, Inc