ims.fieldupdater 3.0.1__py3-none-any.whl → 3.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,362 +1,369 @@
1
- import datetime
2
-
3
- import plone.api as api
4
- from DateTime import DateTime
5
- from Products.Five import BrowserView
6
- from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
7
- from collective.z3cform.datagridfield.row import DictRow
8
- from plone.behavior.interfaces import IBehavior
9
- from plone.dexterity.events import EditFinishedEvent
10
- from plone.dexterity.utils import resolveDottedName
11
- from z3c.form.interfaces import IFieldWidget, NO_VALUE, IDataConverter
12
- from zope.component import getMultiAdapter, getUtilitiesFor, queryUtility
13
- from zope.event import notify
14
- from zope.lifecycleevent import ObjectModifiedEvent
15
- from zope.schema.interfaces import WrongType
16
-
17
- from .. import _
18
-
19
-
20
- def get_behav(name):
21
- behav = queryUtility(IBehavior, name=name)
22
- if behav is not None:
23
- return behav.interface
24
- else:
25
- return resolveDottedName(name)
26
-
27
-
28
- class MassEditForm(BrowserView):
29
- template = ViewPageTemplateFile("mass.pt")
30
- unsupported = None
31
-
32
- def __call__(self):
33
- if not self.request.form.get("form.button.Merge", "") and not self.request.form.get("form.button.Delete", ""):
34
- return self.template({})
35
-
36
- schema = self.request.get("schema", None)
37
- field = self.request.get("field", None)
38
- fkey = self.request.get("fkey", None)
39
- match = self.request.get("match", None)
40
- replacement = self.request.get("replacement_marker", None)
41
-
42
- if self.request.form.get("form.button.Merge", ""):
43
- if replacement:
44
- try:
45
- self.replace_term(schema, field, fkey, match)
46
- except Exception as e:
47
- api.portal.show_message(
48
- message=f"Failed to validate: {e.__repr__()}", request=self.request, type="error"
49
- )
50
- else:
51
- api.portal.show_message(message="Please enter a replacement value.", request=self.request)
52
- elif self.request.form.get("form.button.Delete", ""):
53
- try:
54
- self.delete_term(schema, field, fkey, match)
55
- except Exception as e:
56
- api.portal.show_message(
57
- message=f"Failed to validate: {e.__repr__()}", request=self.request, type="error"
58
- )
59
-
60
- return self.template()
61
-
62
- def get_schemas(self):
63
- """
64
- Our source of schemas is the object_provides KeywordIndex catalog in the catalog as well as Dexterity
65
- Behavior schema cache.
66
- :return: dotted name of interfaces
67
- """
68
- behaviors = tuple([behav[1].interface.__identifier__ for behav in getUtilitiesFor(IBehavior)])
69
- catalog = api.portal.get_tool("portal_catalog")
70
- interfaces = sorted(set(catalog.uniqueValuesFor("object_provides") + behaviors),
71
- key=lambda term: term.split(".")[-1])
72
- for interface in interfaces:
73
- if get_behav(interface).names():
74
- yield {
75
- "id": interface,
76
- "title": interface.split(".")[-1],
77
- }
78
-
79
- def schema_matches(self, schema):
80
- """
81
- Get a count of matches
82
- :param schema: dotted name interface
83
- :return: int
84
- """
85
- catalog = api.portal.get_tool("portal_catalog")
86
- if schema in catalog.uniqueValuesFor("object_provides"):
87
- return len(catalog(object_provides=schema))
88
- else:
89
- return "unknown (cannot get a subset for behavior interfaces)"
90
-
91
- def get_fields(self):
92
- """
93
- Get all fields for a schema
94
- :return: fields
95
- """
96
- schema = self.request.get("schema", None)
97
- if not schema:
98
- return
99
- interface = get_behav(schema)
100
- for name in interface.names():
101
- if interface[name] and hasattr(interface[name], "title"):
102
- yield {"id": name, "title": f"{interface[name].title} [{name}]"}
103
-
104
- def is_dg(self):
105
- """
106
- DataGridField (collective.z3cform.datagridfield) support. Lists with dicts
107
- :return: bool
108
- """
109
- schema = self.request.get("schema", None)
110
- field = self.request.get("field", None)
111
- if not field or not schema:
112
- return
113
- interface = get_behav(schema)
114
- return hasattr(interface[field], "value_type") and isinstance(interface[field].value_type, DictRow)
115
-
116
- def get_dgschema(self):
117
- """Look up the schema used for the dg field"""
118
- schema = self.request.get("schema", None)
119
- field = self.request.get("field", None)
120
- if not field or not schema:
121
- return
122
- interface = get_behav(schema)
123
- return interface[field].value_type.schema
124
-
125
- def get_dgkeys(self):
126
- """
127
- Get DataGridField schema keys
128
- :return:
129
- """
130
- dg_schema = self.get_dgschema()
131
- for dg_field in dg_schema.names():
132
- yield {
133
- "id": dg_field,
134
- "title": dg_schema[dg_field].title,
135
- }
136
-
137
- def get_values(self): # noqa: C901
138
- """
139
- Find all the current values for objects that provide this schema
140
-
141
- :return: values
142
- """
143
- self.unsupported = None
144
- schema = self.request.get("schema", None)
145
- field = self.request.get("field", None)
146
- fkey = self.request.get("fkey", None)
147
-
148
- if (self.is_dg() and not (schema and field and fkey)) or (not self.is_dg() and not (schema and field)):
149
- return
150
- values = set()
151
- catalog = api.portal.get_tool("portal_catalog")
152
- query = catalog(object_provides=schema) if schema in catalog.uniqueValuesFor("object_provides") else catalog()
153
-
154
- for brain in query:
155
- obj = brain.getObject()
156
- field_value = getattr(obj, field, None)
157
- if not field_value:
158
- continue
159
- if field_value and isinstance(field_value, str):
160
- values.add(field_value)
161
- elif field_value and isinstance(field_value, tuple | list):
162
- for item_value in field_value:
163
- if fkey: # dg
164
- if item_value[fkey] and isinstance(item_value[fkey], str):
165
- values.add(item_value[fkey])
166
- else: # regular list or tuple
167
- if item_value and isinstance(item_value, str):
168
- values.add(item_value)
169
- elif field_value and isinstance(field_value, datetime.datetime | datetime.date):
170
- values.add(field_value)
171
- else:
172
- self.unsupported = field_value.__class__.__name__
173
- return sorted(values)
174
-
175
- def results(self):
176
- """
177
- Find all of the content that matches the current selection
178
-
179
- :return: brains
180
- """
181
- schema = self.request.get("schema", None)
182
- field = self.request.get("field", None)
183
- fkey = self.request.get("fkey", None)
184
- match = self.request.get("match", None)
185
- if not schema or not field:
186
- return
187
- if self.is_dg() and not fkey:
188
- return
189
- if not match:
190
- return
191
-
192
- catalog = api.portal.get_tool("portal_catalog")
193
- _results = []
194
- query = catalog(object_provides=schema) if schema in catalog.uniqueValuesFor("object_provides") else catalog()
195
- for brain in query:
196
- obj = brain.getObject()
197
- field_value = getattr(obj, field, None)
198
- checks = [
199
- field_value == match and isinstance(field_value, str), # str
200
- isinstance(field_value, tuple | list) and match in field_value, # iterator
201
- fkey and match in [item_value[fkey] for item_value in field_value], # dg
202
- isinstance(field_value, datetime.date) and DateTime(match).asdatetime().date() == field_value,
203
- isinstance(field_value, datetime.date) and DateTime(match).asdatetime() == field_value
204
- ]
205
- if any(checks):
206
- _results.append(brain)
207
- return _results
208
-
209
- def replace_term(self, schema, field, fkey, match):
210
- """
211
- Replace a field term. For multi valued fields it will replace just the matching part of it. For single
212
- valued fields it replaces the whole field, where it matches. We get the widget from the actual schema field
213
- so we can use it to extract and parse the appropriate value too (!)
214
-
215
- :param schema: dotted name schema (str)
216
- :param field: field
217
- :param fkey: boolean, representing DateGridField
218
- :param match: the value being matched
219
- :return: None
220
- """
221
- widget = self.replacement_widget
222
- replacement = widget.extract()
223
- if replacement is not NO_VALUE:
224
- try:
225
- replacement = IDataConverter(widget).toFieldValue(replacement)
226
- except WrongType:
227
- # for some reason some things that should come in as unicode are coming in as strings
228
- replacement = IDataConverter(widget).toFieldValue(IDataConverter(widget).toWidgetValue(replacement))
229
- if not replacement or replacement is NO_VALUE:
230
- api.portal.show_message(message=_("No replacement value given"), request=self.request, type="error")
231
- return
232
-
233
- results = self.results()
234
- for brain in results:
235
- self.set_value_by_type(brain, schema, field, fkey, match, replacement)
236
- api.portal.show_message(
237
- message=_(f"Replaced term in {len(results)} records"), request=self.request, type="info"
238
- )
239
-
240
- def set_value_by_type(self, brain, schema, field, fkey, match, replacement):
241
- """ Set value based on field value type """
242
- obj = brain.getObject()
243
-
244
- field_value = getattr(obj, field, None)
245
- if isinstance(field_value, str | datetime.date | datetime.datetime):
246
- self.set_value(obj, schema, field, replacement)
247
- elif isinstance(field_value, tuple | list):
248
- if fkey:
249
- for item_value in field_value:
250
- if item_value[fkey] == match:
251
- item_value[fkey] = replacement
252
- self.set_value(obj, schema, field, field_value)
253
- else:
254
- if replacement in field_value:
255
- field_value = [item_value for item_value in field_value if item_value != match]
256
- else:
257
- field_value = [(item_value == match and replacement) or item_value for item_value in
258
- field_value]
259
- self.set_value(obj, schema, field, field_value)
260
-
261
- def delete_term(self, schema, field, fkey, match):
262
- """
263
- Delete this field. For single value fields, this is set to None - it must still pass validation so
264
- required fields should fail.
265
- :param schema: dotted name schema (str)
266
- :param field: field
267
- :param fkey: boolean, representing DateGridField
268
- :param match: the value being matched
269
- :return: None
270
- """
271
-
272
- for brain in self.results():
273
- obj = brain.getObject()
274
- field_value = getattr(obj, field, None)
275
-
276
- if isinstance(field_value, tuple | list):
277
- if fkey:
278
- for item_value in field_value:
279
- if item_value[fkey] == match:
280
- item_value[fkey] = None
281
- # if this was the only value in the row, delete the row
282
- field_value = [
283
- item_value for item_value in field_value if [i for i in list(item_value.values()) if i]
284
- ]
285
- else:
286
- field_value = [item_value for item_value in field_value if item_value != match]
287
- self.set_value(obj, schema, field, field_value)
288
- else:
289
- self.set_value(obj, schema, field, None)
290
- api.portal.show_message(
291
- message=_(f"Removed term in {len(self.results())} records"), request=self.request, type="info"
292
- )
293
-
294
- def set_value(self, obj, dottedname, field, field_value, attempts=0):
295
- """
296
- Set the value for an object's field. Values must be validated as defined in the schema. This can be called
297
- recursively as a sort of hack where zope.schema gets finicky between unicode and str.
298
- :param obj: the object being modified
299
- :param dottedname: non-resolved schema interface
300
- :param field: field being changed
301
- :param field_value: the new value of the field. In this case, for multi values, it is the entire value
302
- :param attempts: recursive attempts made
303
- :return:
304
- """
305
- attempt_limit = 1 # some of the more common validation problems are unicode where it expects ascii
306
- # or vice versa. Try once
307
- schema = get_behav(dottedname)
308
- bound = schema[field].bind(obj)
309
- try:
310
- bound.validate(field_value)
311
- except WrongType as e:
312
- if attempts < attempt_limit:
313
- attempts += 1
314
- if isinstance(field_value, str):
315
- field_value = str(field_value)
316
- return self.set_value(obj, dottedname, field, field_value, attempts)
317
- else:
318
- api.portal.show_message(
319
- message=f"Failed to validate: {e.__repr__()}", request=self.request, type="error"
320
- )
321
- else:
322
- setattr(obj, field, field_value)
323
- notify(ObjectModifiedEvent(obj))
324
- notify(EditFinishedEvent(obj))
325
- obj.reindexObject()
326
-
327
- @property
328
- def replacement_widget(self):
329
- """
330
- Get a widget for use in getting the replacement value. If the widget has a value_type, assume we want to
331
- render just that part. For instance, if it's a list we are only replacing one value in it so a multi value
332
- widget wouldn't make sense. So for schema.List(value_type=Choice()) we would render that Choice
333
- :return: widget
334
- """
335
- if self.is_dg():
336
- fkey = self.request["fkey"]
337
- field = self.get_dgschema()[fkey]
338
- else:
339
- schema = self.request.get("schema", None)
340
- schema = get_behav(schema)
341
- field = self.request.get("field", None)
342
- field = schema[field]
343
- if hasattr(field, "value_type"):
344
- field = field.value_type
345
-
346
- widget = getMultiAdapter((field, self.request), IFieldWidget)
347
- widget.name = "replacement"
348
- widget.update()
349
- return widget
350
-
351
-
352
- class SchemaFinderForm(BrowserView):
353
- def get_types(self):
354
- catalog = api.portal.get_tool("portal_catalog")
355
- return sorted(catalog.uniqueValuesFor("portal_type"))
356
-
357
- def schemas(self):
358
- content_type = self.request["content_type"]
359
- type_info = api.portal.get_tool("portal_types").getTypeInfo(content_type)
360
- yield {"id": type_info.schema, "title": type_info.schema.split(".")[-1]}
361
- for behav in type_info.behaviors:
362
- yield {"id": behav, "title": behav.split(".")[-1]}
1
+ import datetime
2
+
3
+ import plone.api as api
4
+ from collective.z3cform.datagridfield.row import DictRow
5
+ from DateTime import DateTime
6
+ from plone.behavior.interfaces import IBehavior
7
+ from plone.dexterity.events import EditFinishedEvent
8
+ from plone.dexterity.utils import resolveDottedName
9
+ from Products.Five import BrowserView
10
+ from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
11
+ from z3c.form.interfaces import NO_VALUE, IDataConverter, IFieldWidget
12
+ from zope.component import getMultiAdapter, getUtilitiesFor, queryUtility
13
+ from zope.event import notify
14
+ from zope.lifecycleevent import ObjectModifiedEvent
15
+ from zope.schema.interfaces import WrongType
16
+
17
+ from .. import _
18
+
19
+
20
+ def get_behav(name):
21
+ behav = queryUtility(IBehavior, name=name)
22
+ if behav is not None:
23
+ return behav.interface
24
+ else:
25
+ return resolveDottedName(name)
26
+
27
+
28
+ class MassEditForm(BrowserView):
29
+ template = ViewPageTemplateFile("mass.pt")
30
+ unsupported = None
31
+
32
+ def __call__(self):
33
+ if not self.request.form.get("form.button.Merge", "") and not self.request.form.get("form.button.Delete", ""):
34
+ return self.template({})
35
+
36
+ schema = self.request.get("schema", None)
37
+ field = self.request.get("field", None)
38
+ fkey = self.request.get("fkey", None)
39
+ match = self.request.get("match", None)
40
+ replacement = self.request.get("replacement_marker", None)
41
+
42
+ if self.request.form.get("form.button.Merge", ""):
43
+ if replacement:
44
+ try:
45
+ self.replace_term(schema, field, fkey, match)
46
+ except Exception as e:
47
+ api.portal.show_message(
48
+ message=f"Failed to validate: {e.__repr__()}", request=self.request, type="error"
49
+ )
50
+ else:
51
+ api.portal.show_message(message="Please enter a replacement value.", request=self.request)
52
+ elif self.request.form.get("form.button.Delete", ""):
53
+ try:
54
+ self.delete_term(schema, field, fkey, match)
55
+ except Exception as e:
56
+ api.portal.show_message(
57
+ message=f"Failed to validate: {e.__repr__()}", request=self.request, type="error"
58
+ )
59
+
60
+ return self.template()
61
+
62
+ def get_schemas(self):
63
+ """
64
+ Our source of schemas is the object_provides KeywordIndex catalog in the catalog as well as Dexterity
65
+ Behavior schema cache.
66
+ :return: dotted name of interfaces
67
+ """
68
+ behaviors = tuple([behav[1].interface.__identifier__ for behav in getUtilitiesFor(IBehavior)])
69
+ catalog = api.portal.get_tool("portal_catalog")
70
+ interfaces = sorted(
71
+ set(catalog.uniqueValuesFor("object_provides") + behaviors), key=lambda term: term.split(".")[-1]
72
+ )
73
+
74
+ for interface in interfaces:
75
+ try:
76
+ if get_behav(interface).names():
77
+ yield {
78
+ "id": interface,
79
+ "title": interface.split(".")[-1],
80
+ }
81
+ except ModuleNotFoundError:
82
+ api.portal.show_message(
83
+ f"Failed to find interface: {interface}. This is registered as a behavior or noted as a provided interface in the catalog, but the class does not exist.",
84
+ type="error",
85
+ )
86
+
87
+ def schema_matches(self, schema):
88
+ """
89
+ Get a count of matches
90
+ :param schema: dotted name interface
91
+ :return: int
92
+ """
93
+ catalog = api.portal.get_tool("portal_catalog")
94
+ if schema in catalog.uniqueValuesFor("object_provides"):
95
+ return len(catalog(object_provides=schema))
96
+ else:
97
+ return "unknown (cannot get a subset for behavior interfaces)"
98
+
99
+ def get_fields(self):
100
+ """
101
+ Get all fields for a schema
102
+ :return: fields
103
+ """
104
+ schema = self.request.get("schema", None)
105
+ if not schema:
106
+ return
107
+ interface = get_behav(schema)
108
+ for name in interface.names():
109
+ if interface[name] and hasattr(interface[name], "title"):
110
+ yield {"id": name, "title": f"{interface[name].title} [{name}]"}
111
+
112
+ def is_dg(self):
113
+ """
114
+ DataGridField (collective.z3cform.datagridfield) support. Lists with dicts
115
+ :return: bool
116
+ """
117
+ schema = self.request.get("schema", None)
118
+ field = self.request.get("field", None)
119
+ if not field or not schema:
120
+ return
121
+ interface = get_behav(schema)
122
+ return hasattr(interface[field], "value_type") and isinstance(interface[field].value_type, DictRow)
123
+
124
+ def get_dgschema(self):
125
+ """Look up the schema used for the dg field"""
126
+ schema = self.request.get("schema", None)
127
+ field = self.request.get("field", None)
128
+ if not field or not schema:
129
+ return
130
+ interface = get_behav(schema)
131
+ return interface[field].value_type.schema
132
+
133
+ def get_dgkeys(self):
134
+ """
135
+ Get DataGridField schema keys
136
+ :return:
137
+ """
138
+ dg_schema = self.get_dgschema()
139
+ for dg_field in dg_schema.names():
140
+ yield {
141
+ "id": dg_field,
142
+ "title": dg_schema[dg_field].title,
143
+ }
144
+
145
+ def get_values(self): # noqa: C901
146
+ """
147
+ Find all the current values for objects that provide this schema
148
+
149
+ :return: values
150
+ """
151
+ self.unsupported = None
152
+ schema = self.request.get("schema", None)
153
+ field = self.request.get("field", None)
154
+ fkey = self.request.get("fkey", None)
155
+
156
+ if (self.is_dg() and not (schema and field and fkey)) or (not self.is_dg() and not (schema and field)):
157
+ return
158
+ values = set()
159
+ catalog = api.portal.get_tool("portal_catalog")
160
+ query = catalog(object_provides=schema) if schema in catalog.uniqueValuesFor("object_provides") else catalog()
161
+
162
+ for brain in query:
163
+ obj = brain.getObject()
164
+ field_value = getattr(obj, field, None)
165
+ if not field_value:
166
+ continue
167
+ if field_value and isinstance(field_value, str):
168
+ values.add(field_value)
169
+ elif field_value and isinstance(field_value, tuple | list):
170
+ for item_value in field_value:
171
+ if fkey: # dg
172
+ if item_value[fkey] and isinstance(item_value[fkey], str):
173
+ values.add(item_value[fkey])
174
+ else: # regular list or tuple
175
+ if item_value and isinstance(item_value, str):
176
+ values.add(item_value)
177
+ elif field_value and isinstance(field_value, datetime.datetime | datetime.date):
178
+ values.add(field_value)
179
+ else:
180
+ self.unsupported = field_value.__class__.__name__
181
+ return sorted(values)
182
+
183
+ def results(self):
184
+ """
185
+ Find all of the content that matches the current selection
186
+
187
+ :return: brains
188
+ """
189
+ schema = self.request.get("schema", None)
190
+ field = self.request.get("field", None)
191
+ fkey = self.request.get("fkey", None)
192
+ match = self.request.get("match", None)
193
+ if not schema or not field:
194
+ return
195
+ if self.is_dg() and not fkey:
196
+ return
197
+ if not match:
198
+ return
199
+
200
+ catalog = api.portal.get_tool("portal_catalog")
201
+ _results = []
202
+ query = catalog(object_provides=schema) if schema in catalog.uniqueValuesFor("object_provides") else catalog()
203
+ for brain in query:
204
+ obj = brain.getObject()
205
+ field_value = getattr(obj, field, None)
206
+ checks = [
207
+ field_value == match and isinstance(field_value, str), # str
208
+ isinstance(field_value, tuple | list) and match in field_value, # iterator
209
+ fkey and match in [item_value[fkey] for item_value in field_value], # dg
210
+ isinstance(field_value, datetime.date) and DateTime(match).asdatetime().date() == field_value,
211
+ isinstance(field_value, datetime.date) and DateTime(match).asdatetime() == field_value,
212
+ ]
213
+ if any(checks):
214
+ _results.append(brain)
215
+ return _results
216
+
217
+ def replace_term(self, schema, field, fkey, match):
218
+ """
219
+ Replace a field term. For multi valued fields it will replace just the matching part of it. For single
220
+ valued fields it replaces the whole field, where it matches. We get the widget from the actual schema field
221
+ so we can use it to extract and parse the appropriate value too (!)
222
+
223
+ :param schema: dotted name schema (str)
224
+ :param field: field
225
+ :param fkey: boolean, representing DateGridField
226
+ :param match: the value being matched
227
+ :return: None
228
+ """
229
+ widget = self.replacement_widget
230
+ replacement = widget.extract()
231
+ if replacement is not NO_VALUE:
232
+ try:
233
+ replacement = IDataConverter(widget).toFieldValue(replacement)
234
+ except WrongType:
235
+ # for some reason some things that should come in as unicode are coming in as strings
236
+ replacement = IDataConverter(widget).toFieldValue(IDataConverter(widget).toWidgetValue(replacement))
237
+ if not replacement or replacement is NO_VALUE:
238
+ api.portal.show_message(message=_("No replacement value given"), request=self.request, type="error")
239
+ return
240
+
241
+ results = self.results()
242
+ for brain in results:
243
+ self.set_value_by_type(brain, schema, field, fkey, match, replacement)
244
+ api.portal.show_message(
245
+ message=_(f"Replaced term in {len(results)} records"), request=self.request, type="info"
246
+ )
247
+
248
+ def set_value_by_type(self, brain, schema, field, fkey, match, replacement):
249
+ """Set value based on field value type"""
250
+ obj = brain.getObject()
251
+
252
+ field_value = getattr(obj, field, None)
253
+ if isinstance(field_value, str | datetime.date | datetime.datetime):
254
+ self.set_value(obj, schema, field, replacement)
255
+ elif isinstance(field_value, tuple | list):
256
+ if fkey:
257
+ for item_value in field_value:
258
+ if item_value[fkey] == match:
259
+ item_value[fkey] = replacement
260
+ self.set_value(obj, schema, field, field_value)
261
+ else:
262
+ if replacement in field_value:
263
+ field_value = [item_value for item_value in field_value if item_value != match]
264
+ else:
265
+ field_value = [(item_value == match and replacement) or item_value for item_value in field_value]
266
+ self.set_value(obj, schema, field, field_value)
267
+
268
+ def delete_term(self, schema, field, fkey, match):
269
+ """
270
+ Delete this field. For single value fields, this is set to None - it must still pass validation so
271
+ required fields should fail.
272
+ :param schema: dotted name schema (str)
273
+ :param field: field
274
+ :param fkey: boolean, representing DateGridField
275
+ :param match: the value being matched
276
+ :return: None
277
+ """
278
+
279
+ for brain in self.results():
280
+ obj = brain.getObject()
281
+ field_value = getattr(obj, field, None)
282
+
283
+ if isinstance(field_value, tuple | list):
284
+ if fkey:
285
+ for item_value in field_value:
286
+ if item_value[fkey] == match:
287
+ item_value[fkey] = None
288
+ # if this was the only value in the row, delete the row
289
+ field_value = [
290
+ item_value for item_value in field_value if [i for i in list(item_value.values()) if i]
291
+ ]
292
+ else:
293
+ field_value = [item_value for item_value in field_value if item_value != match]
294
+ self.set_value(obj, schema, field, field_value)
295
+ else:
296
+ self.set_value(obj, schema, field, None)
297
+ api.portal.show_message(
298
+ message=_(f"Removed term in {len(self.results())} records"), request=self.request, type="info"
299
+ )
300
+
301
+ def set_value(self, obj, dottedname, field, field_value, attempts=0):
302
+ """
303
+ Set the value for an object's field. Values must be validated as defined in the schema. This can be called
304
+ recursively as a sort of hack where zope.schema gets finicky between unicode and str.
305
+ :param obj: the object being modified
306
+ :param dottedname: non-resolved schema interface
307
+ :param field: field being changed
308
+ :param field_value: the new value of the field. In this case, for multi values, it is the entire value
309
+ :param attempts: recursive attempts made
310
+ :return:
311
+ """
312
+ attempt_limit = 1 # some of the more common validation problems are unicode where it expects ascii
313
+ # or vice versa. Try once
314
+ schema = get_behav(dottedname)
315
+ bound = schema[field].bind(obj)
316
+ try:
317
+ bound.validate(field_value)
318
+ except WrongType as e:
319
+ if attempts < attempt_limit:
320
+ attempts += 1
321
+ if isinstance(field_value, str):
322
+ field_value = str(field_value)
323
+ return self.set_value(obj, dottedname, field, field_value, attempts)
324
+ else:
325
+ api.portal.show_message(
326
+ message=f"Failed to validate: {e.__repr__()}", request=self.request, type="error"
327
+ )
328
+ else:
329
+ setattr(obj, field, field_value)
330
+ notify(ObjectModifiedEvent(obj))
331
+ notify(EditFinishedEvent(obj))
332
+ obj.reindexObject()
333
+
334
+ @property
335
+ def replacement_widget(self):
336
+ """
337
+ Get a widget for use in getting the replacement value. If the widget has a value_type, assume we want to
338
+ render just that part. For instance, if it's a list we are only replacing one value in it so a multi value
339
+ widget wouldn't make sense. So for schema.List(value_type=Choice()) we would render that Choice
340
+ :return: widget
341
+ """
342
+ if self.is_dg():
343
+ fkey = self.request["fkey"]
344
+ field = self.get_dgschema()[fkey]
345
+ else:
346
+ schema = self.request.get("schema", None)
347
+ schema = get_behav(schema)
348
+ field = self.request.get("field", None)
349
+ field = schema[field]
350
+ if hasattr(field, "value_type"):
351
+ field = field.value_type
352
+
353
+ widget = getMultiAdapter((field, self.request), IFieldWidget)
354
+ widget.name = "replacement"
355
+ widget.update()
356
+ return widget
357
+
358
+
359
+ class SchemaFinderForm(BrowserView):
360
+ def get_types(self):
361
+ catalog = api.portal.get_tool("portal_catalog")
362
+ return sorted(catalog.uniqueValuesFor("portal_type"))
363
+
364
+ def schemas(self):
365
+ content_type = self.request["content_type"]
366
+ type_info = api.portal.get_tool("portal_types").getTypeInfo(content_type)
367
+ yield {"id": type_info.schema, "title": type_info.schema.split(".")[-1]}
368
+ for behav in type_info.behaviors:
369
+ yield {"id": behav, "title": behav.split(".")[-1]}