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