Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/wtforms/fields/list.py: 20%
103 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:32 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:32 +0000
1import itertools
3from .. import widgets
4from .core import Field
5from .core import UnboundField
6from wtforms.utils import unset_value
8__all__ = ("FieldList",)
11class FieldList(Field):
12 """
13 Encapsulate an ordered list of multiple instances of the same field type,
14 keeping data as a list.
16 >>> authors = FieldList(StringField('Name', [validators.DataRequired()]))
18 :param unbound_field:
19 A partially-instantiated field definition, just like that would be
20 defined on a form directly.
21 :param min_entries:
22 if provided, always have at least this many entries on the field,
23 creating blank ones if the provided input does not specify a sufficient
24 amount.
25 :param max_entries:
26 accept no more than this many entries as input, even if more exist in
27 formdata.
28 :param separator:
29 A string which will be suffixed to this field's name to create the
30 prefix to enclosed list entries. The default is fine for most uses.
31 """
33 widget = widgets.ListWidget()
35 def __init__(
36 self,
37 unbound_field,
38 label=None,
39 validators=None,
40 min_entries=0,
41 max_entries=None,
42 separator="-",
43 default=(),
44 **kwargs,
45 ):
46 super().__init__(label, validators, default=default, **kwargs)
47 if self.filters:
48 raise TypeError(
49 "FieldList does not accept any filters. Instead, define"
50 " them on the enclosed field."
51 )
52 assert isinstance(
53 unbound_field, UnboundField
54 ), "Field must be unbound, not a field class"
55 self.unbound_field = unbound_field
56 self.min_entries = min_entries
57 self.max_entries = max_entries
58 self.last_index = -1
59 self._prefix = kwargs.get("_prefix", "")
60 self._separator = separator
61 self._field_separator = unbound_field.kwargs.get("separator", "-")
63 def process(self, formdata, data=unset_value, extra_filters=None):
64 if extra_filters:
65 raise TypeError(
66 "FieldList does not accept any filters. Instead, define"
67 " them on the enclosed field."
68 )
70 self.entries = []
71 if data is unset_value or not data:
72 try:
73 data = self.default()
74 except TypeError:
75 data = self.default
77 self.object_data = data
79 if formdata:
80 indices = sorted(set(self._extract_indices(self.name, formdata)))
81 if self.max_entries:
82 indices = indices[: self.max_entries]
84 idata = iter(data)
85 for index in indices:
86 try:
87 obj_data = next(idata)
88 except StopIteration:
89 obj_data = unset_value
90 self._add_entry(formdata, obj_data, index=index)
91 else:
92 for obj_data in data:
93 self._add_entry(formdata, obj_data)
95 while len(self.entries) < self.min_entries:
96 self._add_entry(formdata)
98 def _extract_indices(self, prefix, formdata):
99 """
100 Yield indices of any keys with given prefix.
102 formdata must be an object which will produce keys when iterated. For
103 example, if field 'foo' contains keys 'foo-0-bar', 'foo-1-baz', then
104 the numbers 0 and 1 will be yielded, but not necessarily in order.
105 """
106 offset = len(prefix) + 1
107 for k in formdata:
108 if k.startswith(prefix):
109 k = k[offset:].split(self._field_separator, 1)[0]
110 if k.isdigit():
111 yield int(k)
113 def validate(self, form, extra_validators=()):
114 """
115 Validate this FieldList.
117 Note that FieldList validation differs from normal field validation in
118 that FieldList validates all its enclosed fields first before running any
119 of its own validators.
120 """
121 self.errors = []
123 # Run validators on all entries within
124 for subfield in self.entries:
125 subfield.validate(form)
126 self.errors.append(subfield.errors)
128 if not any(x for x in self.errors):
129 self.errors = []
131 chain = itertools.chain(self.validators, extra_validators)
132 self._run_validation_chain(form, chain)
134 return len(self.errors) == 0
136 def populate_obj(self, obj, name):
137 values = getattr(obj, name, None)
138 try:
139 ivalues = iter(values)
140 except TypeError:
141 ivalues = iter([])
143 candidates = itertools.chain(ivalues, itertools.repeat(None))
144 _fake = type("_fake", (object,), {})
145 output = []
146 for field, data in zip(self.entries, candidates):
147 fake_obj = _fake()
148 fake_obj.data = data
149 field.populate_obj(fake_obj, "data")
150 output.append(fake_obj.data)
152 setattr(obj, name, output)
154 def _add_entry(self, formdata=None, data=unset_value, index=None):
155 assert (
156 not self.max_entries or len(self.entries) < self.max_entries
157 ), "You cannot have more than max_entries entries in this FieldList"
158 if index is None:
159 index = self.last_index + 1
160 self.last_index = index
161 name = f"{self.short_name}{self._separator}{index}"
162 id = f"{self.id}{self._separator}{index}"
163 field = self.unbound_field.bind(
164 form=None,
165 name=name,
166 prefix=self._prefix,
167 id=id,
168 _meta=self.meta,
169 translations=self._translations,
170 )
171 field.process(formdata, data)
172 self.entries.append(field)
173 return field
175 def append_entry(self, data=unset_value):
176 """
177 Create a new entry with optional default data.
179 Entries added in this way will *not* receive formdata however, and can
180 only receive object data.
181 """
182 return self._add_entry(data=data)
184 def pop_entry(self):
185 """Removes the last entry from the list and returns it."""
186 entry = self.entries.pop()
187 self.last_index -= 1
188 return entry
190 def __iter__(self):
191 return iter(self.entries)
193 def __len__(self):
194 return len(self.entries)
196 def __getitem__(self, index):
197 return self.entries[index]
199 @property
200 def data(self):
201 return [f.data for f in self.entries]