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