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

1import itertools 

2 

3from .. import widgets 

4from .core import Field 

5from .core import UnboundField 

6from wtforms.utils import unset_value 

7 

8__all__ = ("FieldList",) 

9 

10 

11class FieldList(Field): 

12 """ 

13 Encapsulate an ordered list of multiple instances of the same field type, 

14 keeping data as a list. 

15 

16 >>> authors = FieldList(StringField('Name', [validators.DataRequired()])) 

17 

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 """ 

32 

33 widget = widgets.ListWidget() 

34 

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", "-") 

62 

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 ) 

69 

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 

76 

77 self.object_data = data 

78 

79 if formdata: 

80 indices = sorted(set(self._extract_indices(self.name, formdata))) 

81 if self.max_entries: 

82 indices = indices[: self.max_entries] 

83 

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) 

94 

95 while len(self.entries) < self.min_entries: 

96 self._add_entry(formdata) 

97 

98 def _extract_indices(self, prefix, formdata): 

99 """ 

100 Yield indices of any keys with given prefix. 

101 

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) 

112 

113 def validate(self, form, extra_validators=()): 

114 """ 

115 Validate this FieldList. 

116 

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 = [] 

122 

123 # Run validators on all entries within 

124 for subfield in self.entries: 

125 subfield.validate(form) 

126 self.errors.append(subfield.errors) 

127 

128 if not any(x for x in self.errors): 

129 self.errors = [] 

130 

131 chain = itertools.chain(self.validators, extra_validators) 

132 self._run_validation_chain(form, chain) 

133 

134 return len(self.errors) == 0 

135 

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([]) 

142 

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) 

151 

152 setattr(obj, name, output) 

153 

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 

174 

175 def append_entry(self, data=unset_value): 

176 """ 

177 Create a new entry with optional default data. 

178 

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) 

183 

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 

189 

190 def __iter__(self): 

191 return iter(self.entries) 

192 

193 def __len__(self): 

194 return len(self.entries) 

195 

196 def __getitem__(self, index): 

197 return self.entries[index] 

198 

199 @property 

200 def data(self): 

201 return [f.data for f in self.entries]