ims.fieldupdater 3.0.0__tar.gz → 3.0.1__tar.gz
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.
- ims.fieldupdater-3.0.1/.gitignore +55 -0
- {ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/PKG-INFO +20 -17
- ims.fieldupdater-3.0.1/ims/fieldupdater/__init__.py +4 -0
- {ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/ims/fieldupdater/browser/mass.pt +1 -1
- {ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/ims/fieldupdater/browser/mass.py +109 -117
- ims.fieldupdater-3.0.1/ims/fieldupdater/testing.py +53 -0
- ims.fieldupdater-3.0.1/pyproject.toml +132 -0
- ims.fieldupdater-3.0.0/MANIFEST.in +0 -2
- ims.fieldupdater-3.0.0/README.md +0 -21
- ims.fieldupdater-3.0.0/ims/fieldupdater/__init__.py +0 -2
- ims.fieldupdater-3.0.0/ims/fieldupdater/testing.py +0 -38
- ims.fieldupdater-3.0.0/ims/fieldupdater/tests/__init__.py +0 -1
- ims.fieldupdater-3.0.0/ims/fieldupdater/tests/base.py +0 -55
- ims.fieldupdater-3.0.0/ims/fieldupdater/tests/interfaces.py +0 -38
- ims.fieldupdater-3.0.0/ims/fieldupdater/tests/test_mass.py +0 -145
- ims.fieldupdater-3.0.0/ims/fieldupdater/tests/test_uninstall.py +0 -23
- ims.fieldupdater-3.0.0/ims.fieldupdater.egg-info/PKG-INFO +0 -17
- ims.fieldupdater-3.0.0/ims.fieldupdater.egg-info/SOURCES.txt +0 -26
- ims.fieldupdater-3.0.0/ims.fieldupdater.egg-info/dependency_links.txt +0 -1
- ims.fieldupdater-3.0.0/ims.fieldupdater.egg-info/entry_points.txt +0 -2
- ims.fieldupdater-3.0.0/ims.fieldupdater.egg-info/requires.txt +0 -6
- ims.fieldupdater-3.0.0/ims.fieldupdater.egg-info/top_level.txt +0 -1
- ims.fieldupdater-3.0.0/pyproject.toml +0 -46
- ims.fieldupdater-3.0.0/setup.cfg +0 -7
- {ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/ims/fieldupdater/browser/__init__.py +0 -0
- {ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/ims/fieldupdater/browser/configure.zcml +0 -0
- {ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/ims/fieldupdater/browser/schema.pt +0 -0
- {ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/ims/fieldupdater/configure.zcml +0 -0
- {ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/ims/fieldupdater/profiles/default/controlpanel.xml +0 -0
- {ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/ims/fieldupdater/profiles/default/metadata.xml +0 -0
- {ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/ims/fieldupdater/profiles/uninstall/controlpanel.xml +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
*.bak
|
|
2
|
+
# Byte-compiled / optimized / DLL files
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
env/
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
.installed.cfg
|
|
24
|
+
*.egg
|
|
25
|
+
|
|
26
|
+
# PyInstaller
|
|
27
|
+
# Usually these files are written by a python script from a template
|
|
28
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
29
|
+
*.manifest
|
|
30
|
+
*.spec
|
|
31
|
+
|
|
32
|
+
# Installer logs
|
|
33
|
+
pip-log.txt
|
|
34
|
+
pip-delete-this-directory.txt
|
|
35
|
+
|
|
36
|
+
# Unit test / coverage reports
|
|
37
|
+
htmlcov/
|
|
38
|
+
.tox/
|
|
39
|
+
.coverage
|
|
40
|
+
.cache
|
|
41
|
+
nosetests.xml
|
|
42
|
+
coverage.xml
|
|
43
|
+
|
|
44
|
+
# Translations
|
|
45
|
+
*.mo
|
|
46
|
+
*.pot
|
|
47
|
+
|
|
48
|
+
# Django stuff:
|
|
49
|
+
*.log
|
|
50
|
+
|
|
51
|
+
# Sphinx documentation
|
|
52
|
+
docs/_build/
|
|
53
|
+
|
|
54
|
+
# PyBuilder
|
|
55
|
+
target/
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name: ims.fieldupdater
|
|
3
|
-
Version: 3.0.
|
|
4
|
-
Summary: Update all objects in Plone based on a schema/field strategy
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Project-URL:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Classifier: Framework :: Plone :: 6.0
|
|
11
|
-
Classifier:
|
|
12
|
-
|
|
13
|
-
Requires-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
Requires-Dist:
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ims.fieldupdater
|
|
3
|
+
Version: 3.0.1
|
|
4
|
+
Summary: Update all objects in Plone based on a schema/field strategy
|
|
5
|
+
Project-URL: homepage, https://git.imsweb.com/plone/ims.fieldupdater
|
|
6
|
+
Project-URL: documentation, https://git.imsweb.com/plone/ims.fieldupdater
|
|
7
|
+
Project-URL: repository, https://git.imsweb.com/plone/ims.fieldupdater
|
|
8
|
+
Author-email: Eric Wohnlich <wohnlice@imsweb.com>
|
|
9
|
+
License: GPL
|
|
10
|
+
Classifier: Framework :: Plone :: 6.0
|
|
11
|
+
Classifier: Framework :: Plone :: 6.1
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Requires-Dist: plone>=6.0.4
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: formencode; extra == 'test'
|
|
17
|
+
Requires-Dist: plone-app-robotframework; extra == 'test'
|
|
18
|
+
Requires-Dist: plone-app-testing; extra == 'test'
|
|
19
|
+
Requires-Dist: plone-mocktestcase; extra == 'test'
|
|
20
|
+
Requires-Dist: pytest-plone; extra == 'test'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US"
|
|
2
2
|
lang="en-US"
|
|
3
|
-
metal:use-macro="context/prefs_main_template/macros/master"
|
|
3
|
+
metal:use-macro="context/prefs_main_template/macros/master">
|
|
4
4
|
<tal:icon tal:replace="structure python:icons.tag('plone-statusmessage-info', tag_alt='status', tag_class='statusmessage-icon mb-1 me-2')" />
|
|
5
5
|
i18n:domain="ims.fieldupdater">
|
|
6
6
|
|
|
@@ -26,34 +26,36 @@ def get_behav(name):
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class MassEditForm(BrowserView):
|
|
29
|
-
template = ViewPageTemplateFile(
|
|
29
|
+
template = ViewPageTemplateFile("mass.pt")
|
|
30
30
|
unsupported = None
|
|
31
31
|
|
|
32
32
|
def __call__(self):
|
|
33
|
-
if not self.request.form.get(
|
|
33
|
+
if not self.request.form.get("form.button.Merge", "") and not self.request.form.get("form.button.Delete", ""):
|
|
34
34
|
return self.template({})
|
|
35
35
|
|
|
36
|
-
schema = self.request.get(
|
|
37
|
-
field = self.request.get(
|
|
38
|
-
fkey = self.request.get(
|
|
39
|
-
match = self.request.get(
|
|
40
|
-
replacement = self.request.get(
|
|
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
41
|
|
|
42
|
-
if self.request.form.get(
|
|
42
|
+
if self.request.form.get("form.button.Merge", ""):
|
|
43
43
|
if replacement:
|
|
44
44
|
try:
|
|
45
45
|
self.replace_term(schema, field, fkey, match)
|
|
46
46
|
except Exception as e:
|
|
47
|
-
api.portal.show_message(
|
|
48
|
-
|
|
47
|
+
api.portal.show_message(
|
|
48
|
+
message=f"Failed to validate: {e.__repr__()}", request=self.request, type="error"
|
|
49
|
+
)
|
|
49
50
|
else:
|
|
50
|
-
api.portal.show_message(message=
|
|
51
|
-
elif self.request.form.get(
|
|
51
|
+
api.portal.show_message(message="Please enter a replacement value.", request=self.request)
|
|
52
|
+
elif self.request.form.get("form.button.Delete", ""):
|
|
52
53
|
try:
|
|
53
54
|
self.delete_term(schema, field, fkey, match)
|
|
54
55
|
except Exception as e:
|
|
55
|
-
api.portal.show_message(
|
|
56
|
-
|
|
56
|
+
api.portal.show_message(
|
|
57
|
+
message=f"Failed to validate: {e.__repr__()}", request=self.request, type="error"
|
|
58
|
+
)
|
|
57
59
|
|
|
58
60
|
return self.template()
|
|
59
61
|
|
|
@@ -64,14 +66,14 @@ class MassEditForm(BrowserView):
|
|
|
64
66
|
:return: dotted name of interfaces
|
|
65
67
|
"""
|
|
66
68
|
behaviors = tuple([behav[1].interface.__identifier__ for behav in getUtilitiesFor(IBehavior)])
|
|
67
|
-
catalog = api.portal.get_tool(
|
|
68
|
-
interfaces = sorted(
|
|
69
|
-
key=lambda term: term.split(
|
|
69
|
+
catalog = api.portal.get_tool("portal_catalog")
|
|
70
|
+
interfaces = sorted(set(catalog.uniqueValuesFor("object_provides") + behaviors),
|
|
71
|
+
key=lambda term: term.split(".")[-1])
|
|
70
72
|
for interface in interfaces:
|
|
71
73
|
if get_behav(interface).names():
|
|
72
74
|
yield {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
"id": interface,
|
|
76
|
+
"title": interface.split(".")[-1],
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
def schema_matches(self, schema):
|
|
@@ -80,45 +82,41 @@ class MassEditForm(BrowserView):
|
|
|
80
82
|
:param schema: dotted name interface
|
|
81
83
|
:return: int
|
|
82
84
|
"""
|
|
83
|
-
catalog = api.portal.get_tool(
|
|
84
|
-
if schema in catalog.uniqueValuesFor(
|
|
85
|
+
catalog = api.portal.get_tool("portal_catalog")
|
|
86
|
+
if schema in catalog.uniqueValuesFor("object_provides"):
|
|
85
87
|
return len(catalog(object_provides=schema))
|
|
86
88
|
else:
|
|
87
|
-
return
|
|
89
|
+
return "unknown (cannot get a subset for behavior interfaces)"
|
|
88
90
|
|
|
89
91
|
def get_fields(self):
|
|
90
92
|
"""
|
|
91
93
|
Get all fields for a schema
|
|
92
94
|
:return: fields
|
|
93
95
|
"""
|
|
94
|
-
schema = self.request.get(
|
|
96
|
+
schema = self.request.get("schema", None)
|
|
95
97
|
if not schema:
|
|
96
98
|
return
|
|
97
99
|
interface = get_behav(schema)
|
|
98
100
|
for name in interface.names():
|
|
99
|
-
if interface[name] and hasattr(interface[name],
|
|
100
|
-
yield {
|
|
101
|
-
'id': name,
|
|
102
|
-
'title': '%s [%s]' % (interface[name].title, name)
|
|
103
|
-
}
|
|
101
|
+
if interface[name] and hasattr(interface[name], "title"):
|
|
102
|
+
yield {"id": name, "title": f"{interface[name].title} [{name}]"}
|
|
104
103
|
|
|
105
104
|
def is_dg(self):
|
|
106
105
|
"""
|
|
107
106
|
DataGridField (collective.z3cform.datagridfield) support. Lists with dicts
|
|
108
107
|
:return: bool
|
|
109
108
|
"""
|
|
110
|
-
schema = self.request.get(
|
|
111
|
-
field = self.request.get(
|
|
109
|
+
schema = self.request.get("schema", None)
|
|
110
|
+
field = self.request.get("field", None)
|
|
112
111
|
if not field or not schema:
|
|
113
112
|
return
|
|
114
113
|
interface = get_behav(schema)
|
|
115
|
-
return hasattr(interface[field],
|
|
114
|
+
return hasattr(interface[field], "value_type") and isinstance(interface[field].value_type, DictRow)
|
|
116
115
|
|
|
117
116
|
def get_dgschema(self):
|
|
118
|
-
"""
|
|
119
|
-
""
|
|
120
|
-
|
|
121
|
-
field = self.request.get('field', None)
|
|
117
|
+
"""Look up the schema used for the dg field"""
|
|
118
|
+
schema = self.request.get("schema", None)
|
|
119
|
+
field = self.request.get("field", None)
|
|
122
120
|
if not field or not schema:
|
|
123
121
|
return
|
|
124
122
|
interface = get_behav(schema)
|
|
@@ -132,31 +130,27 @@ class MassEditForm(BrowserView):
|
|
|
132
130
|
dg_schema = self.get_dgschema()
|
|
133
131
|
for dg_field in dg_schema.names():
|
|
134
132
|
yield {
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
"id": dg_field,
|
|
134
|
+
"title": dg_schema[dg_field].title,
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
def get_values(self):
|
|
137
|
+
def get_values(self): # noqa: C901
|
|
140
138
|
"""
|
|
141
|
-
Find all
|
|
139
|
+
Find all the current values for objects that provide this schema
|
|
142
140
|
|
|
143
141
|
:return: values
|
|
144
142
|
"""
|
|
145
143
|
self.unsupported = None
|
|
146
|
-
schema = self.request.get(
|
|
147
|
-
field = self.request.get(
|
|
148
|
-
fkey = self.request.get(
|
|
144
|
+
schema = self.request.get("schema", None)
|
|
145
|
+
field = self.request.get("field", None)
|
|
146
|
+
fkey = self.request.get("fkey", None)
|
|
149
147
|
|
|
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):
|
|
148
|
+
if (self.is_dg() and not (schema and field and fkey)) or (not self.is_dg() and not (schema and field)):
|
|
153
149
|
return
|
|
154
150
|
values = set()
|
|
155
|
-
catalog = api.portal.get_tool(
|
|
156
|
-
if schema in catalog.uniqueValuesFor(
|
|
157
|
-
|
|
158
|
-
else:
|
|
159
|
-
query = catalog()
|
|
151
|
+
catalog = api.portal.get_tool("portal_catalog")
|
|
152
|
+
query = catalog(object_provides=schema) if schema in catalog.uniqueValuesFor("object_provides") else catalog()
|
|
153
|
+
|
|
160
154
|
for brain in query:
|
|
161
155
|
obj = brain.getObject()
|
|
162
156
|
field_value = getattr(obj, field, None)
|
|
@@ -164,19 +158,19 @@ class MassEditForm(BrowserView):
|
|
|
164
158
|
continue
|
|
165
159
|
if field_value and isinstance(field_value, str):
|
|
166
160
|
values.add(field_value)
|
|
167
|
-
elif field_value and isinstance(field_value, tuple
|
|
161
|
+
elif field_value and isinstance(field_value, tuple | list):
|
|
168
162
|
for item_value in field_value:
|
|
169
|
-
if fkey:
|
|
163
|
+
if fkey: # dg
|
|
170
164
|
if item_value[fkey] and isinstance(item_value[fkey], str):
|
|
171
165
|
values.add(item_value[fkey])
|
|
172
|
-
else:
|
|
166
|
+
else: # regular list or tuple
|
|
173
167
|
if item_value and isinstance(item_value, str):
|
|
174
168
|
values.add(item_value)
|
|
175
|
-
elif field_value and
|
|
169
|
+
elif field_value and isinstance(field_value, datetime.datetime | datetime.date):
|
|
176
170
|
values.add(field_value)
|
|
177
171
|
else:
|
|
178
172
|
self.unsupported = field_value.__class__.__name__
|
|
179
|
-
return sorted(
|
|
173
|
+
return sorted(values)
|
|
180
174
|
|
|
181
175
|
def results(self):
|
|
182
176
|
"""
|
|
@@ -184,10 +178,10 @@ class MassEditForm(BrowserView):
|
|
|
184
178
|
|
|
185
179
|
:return: brains
|
|
186
180
|
"""
|
|
187
|
-
schema = self.request.get(
|
|
188
|
-
field = self.request.get(
|
|
189
|
-
fkey = self.request.get(
|
|
190
|
-
match = self.request.get(
|
|
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)
|
|
191
185
|
if not schema or not field:
|
|
192
186
|
return
|
|
193
187
|
if self.is_dg() and not fkey:
|
|
@@ -195,24 +189,20 @@ class MassEditForm(BrowserView):
|
|
|
195
189
|
if not match:
|
|
196
190
|
return
|
|
197
191
|
|
|
198
|
-
catalog = api.portal.get_tool(
|
|
192
|
+
catalog = api.portal.get_tool("portal_catalog")
|
|
199
193
|
_results = []
|
|
200
|
-
if schema in catalog.uniqueValuesFor(
|
|
201
|
-
query = catalog(object_provides=schema)
|
|
202
|
-
else:
|
|
203
|
-
query = catalog()
|
|
194
|
+
query = catalog(object_provides=schema) if schema in catalog.uniqueValuesFor("object_provides") else catalog()
|
|
204
195
|
for brain in query:
|
|
205
196
|
obj = brain.getObject()
|
|
206
197
|
field_value = getattr(obj, field, None)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
elif isinstance(field_value, datetime.date) and DateTime(match).asdatetime() == field_value:
|
|
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):
|
|
216
206
|
_results.append(brain)
|
|
217
207
|
return _results
|
|
218
208
|
|
|
@@ -237,31 +227,36 @@ class MassEditForm(BrowserView):
|
|
|
237
227
|
# for some reason some things that should come in as unicode are coming in as strings
|
|
238
228
|
replacement = IDataConverter(widget).toFieldValue(IDataConverter(widget).toWidgetValue(replacement))
|
|
239
229
|
if not replacement or replacement is NO_VALUE:
|
|
240
|
-
api.portal.show_message(message=_(
|
|
230
|
+
api.portal.show_message(message=_("No replacement value given"), request=self.request, type="error")
|
|
241
231
|
return
|
|
242
232
|
|
|
243
233
|
results = self.results()
|
|
244
234
|
for brain in results:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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]
|
|
257
256
|
else:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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')
|
|
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)
|
|
265
260
|
|
|
266
261
|
def delete_term(self, schema, field, fkey, match):
|
|
267
262
|
"""
|
|
@@ -278,21 +273,23 @@ class MassEditForm(BrowserView):
|
|
|
278
273
|
obj = brain.getObject()
|
|
279
274
|
field_value = getattr(obj, field, None)
|
|
280
275
|
|
|
281
|
-
if isinstance(field_value, tuple
|
|
276
|
+
if isinstance(field_value, tuple | list):
|
|
282
277
|
if fkey:
|
|
283
278
|
for item_value in field_value:
|
|
284
279
|
if item_value[fkey] == match:
|
|
285
280
|
item_value[fkey] = None
|
|
286
281
|
# if this was the only value in the row, delete the row
|
|
287
|
-
field_value = [
|
|
288
|
-
|
|
282
|
+
field_value = [
|
|
283
|
+
item_value for item_value in field_value if [i for i in list(item_value.values()) if i]
|
|
284
|
+
]
|
|
289
285
|
else:
|
|
290
286
|
field_value = [item_value for item_value in field_value if item_value != match]
|
|
291
287
|
self.set_value(obj, schema, field, field_value)
|
|
292
288
|
else:
|
|
293
289
|
self.set_value(obj, schema, field, None)
|
|
294
|
-
api.portal.show_message(
|
|
295
|
-
|
|
290
|
+
api.portal.show_message(
|
|
291
|
+
message=_(f"Removed term in {len(self.results())} records"), request=self.request, type="info"
|
|
292
|
+
)
|
|
296
293
|
|
|
297
294
|
def set_value(self, obj, dottedname, field, field_value, attempts=0):
|
|
298
295
|
"""
|
|
@@ -318,8 +315,9 @@ class MassEditForm(BrowserView):
|
|
|
318
315
|
field_value = str(field_value)
|
|
319
316
|
return self.set_value(obj, dottedname, field, field_value, attempts)
|
|
320
317
|
else:
|
|
321
|
-
api.portal.show_message(
|
|
322
|
-
|
|
318
|
+
api.portal.show_message(
|
|
319
|
+
message=f"Failed to validate: {e.__repr__()}", request=self.request, type="error"
|
|
320
|
+
)
|
|
323
321
|
else:
|
|
324
322
|
setattr(obj, field, field_value)
|
|
325
323
|
notify(ObjectModifiedEvent(obj))
|
|
@@ -335,36 +333,30 @@ class MassEditForm(BrowserView):
|
|
|
335
333
|
:return: widget
|
|
336
334
|
"""
|
|
337
335
|
if self.is_dg():
|
|
338
|
-
fkey = self.request[
|
|
336
|
+
fkey = self.request["fkey"]
|
|
339
337
|
field = self.get_dgschema()[fkey]
|
|
340
338
|
else:
|
|
341
|
-
schema = self.request.get(
|
|
339
|
+
schema = self.request.get("schema", None)
|
|
342
340
|
schema = get_behav(schema)
|
|
343
|
-
field = self.request.get(
|
|
341
|
+
field = self.request.get("field", None)
|
|
344
342
|
field = schema[field]
|
|
345
|
-
if hasattr(field,
|
|
343
|
+
if hasattr(field, "value_type"):
|
|
346
344
|
field = field.value_type
|
|
347
345
|
|
|
348
346
|
widget = getMultiAdapter((field, self.request), IFieldWidget)
|
|
349
|
-
widget.name =
|
|
347
|
+
widget.name = "replacement"
|
|
350
348
|
widget.update()
|
|
351
349
|
return widget
|
|
352
350
|
|
|
353
351
|
|
|
354
352
|
class SchemaFinderForm(BrowserView):
|
|
355
353
|
def get_types(self):
|
|
356
|
-
catalog = api.portal.get_tool(
|
|
357
|
-
return sorted(catalog.uniqueValuesFor(
|
|
354
|
+
catalog = api.portal.get_tool("portal_catalog")
|
|
355
|
+
return sorted(catalog.uniqueValuesFor("portal_type"))
|
|
358
356
|
|
|
359
357
|
def schemas(self):
|
|
360
|
-
content_type = self.request[
|
|
361
|
-
type_info = api.portal.get_tool(
|
|
362
|
-
yield {
|
|
363
|
-
'id': type_info.schema,
|
|
364
|
-
'title': type_info.schema.split('.')[-1]
|
|
365
|
-
}
|
|
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]}
|
|
366
361
|
for behav in type_info.behaviors:
|
|
367
|
-
yield {
|
|
368
|
-
'id': behav,
|
|
369
|
-
'title': behav.split('.')[-1]
|
|
370
|
-
}
|
|
362
|
+
yield {"id": behav, "title": behav.split(".")[-1]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
|
|
2
|
+
from plone.app.robotframework.testing import REMOTE_LIBRARY_BUNDLE_FIXTURE
|
|
3
|
+
from plone.app.testing import FunctionalTesting
|
|
4
|
+
from plone.app.testing import IntegrationTesting
|
|
5
|
+
from plone.app.testing import PloneSandboxLayer
|
|
6
|
+
from plone.app.testing import applyProfile
|
|
7
|
+
from plone.protect import auto
|
|
8
|
+
from plone.testing.zope import WSGI_SERVER_FIXTURE
|
|
9
|
+
|
|
10
|
+
import ims.fieldupdater
|
|
11
|
+
|
|
12
|
+
ORIGINAL_CSRF_DISABLED = auto.CSRF_DISABLED
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FieldUpdaterLayer(PloneSandboxLayer):
|
|
16
|
+
defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)
|
|
17
|
+
|
|
18
|
+
def setUpZope(self, app, configurationContext):
|
|
19
|
+
auto.CSRF_DISABLED = True
|
|
20
|
+
self.loadZCML(package=ims.fieldupdater)
|
|
21
|
+
|
|
22
|
+
def tearDownZope(self, app):
|
|
23
|
+
auto.CSRF_DISABLED = ORIGINAL_CSRF_DISABLED
|
|
24
|
+
|
|
25
|
+
def setUpPloneSite(self, portal):
|
|
26
|
+
applyProfile(portal, "ims.fieldupdater:default")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
FIXTURE = FieldUpdaterLayer()
|
|
30
|
+
|
|
31
|
+
INTEGRATION_TESTING = IntegrationTesting(
|
|
32
|
+
bases=(FIXTURE,),
|
|
33
|
+
name="FieldUpdaterLayer:IntegrationTesting",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
FUNCTIONAL_TESTING = FunctionalTesting(
|
|
37
|
+
bases=(FIXTURE, WSGI_SERVER_FIXTURE),
|
|
38
|
+
name="FieldUpdaterLayer:FunctionalTesting",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
RESTAPI_TESTING = FunctionalTesting(
|
|
42
|
+
bases=(FIXTURE, WSGI_SERVER_FIXTURE),
|
|
43
|
+
name="FieldUpdaterLayer:RestAPITesting",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
ACCEPTANCE_TESTING = FunctionalTesting(
|
|
47
|
+
bases=(
|
|
48
|
+
FIXTURE,
|
|
49
|
+
REMOTE_LIBRARY_BUNDLE_FIXTURE,
|
|
50
|
+
WSGI_SERVER_FIXTURE,
|
|
51
|
+
),
|
|
52
|
+
name="FieldUpdaterLayer:AcceptanceTesting",
|
|
53
|
+
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ims.fieldupdater"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
requires-python = ">=3.8"
|
|
9
|
+
description = "Update all objects in Plone based on a schema/field strategy"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Framework :: Plone :: 6.0",
|
|
12
|
+
"Framework :: Plone :: 6.1",
|
|
13
|
+
"Programming Language :: Python",
|
|
14
|
+
]
|
|
15
|
+
authors = [
|
|
16
|
+
{ name = "Eric Wohnlich", email = "wohnlice@imsweb.com" }
|
|
17
|
+
]
|
|
18
|
+
license = { text = "GPL" }
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"plone>=6.0.4"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
homepage = "https://git.imsweb.com/plone/ims.fieldupdater"
|
|
26
|
+
documentation = "https://git.imsweb.com/plone/ims.fieldupdater"
|
|
27
|
+
repository = "https://git.imsweb.com/plone/ims.fieldupdater"
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = ["plone.app.testing", "plone.mocktestcase", "formencode", "pytest-plone", "plone.app.robotframework"]
|
|
31
|
+
|
|
32
|
+
[project.entry-points."plone.autoinclude.plugin"]
|
|
33
|
+
target = "plone"
|
|
34
|
+
|
|
35
|
+
[tool.bandit]
|
|
36
|
+
exclude_dirs = ["*/tests/*"]
|
|
37
|
+
skips = ["B313", "B314", "B315", "B316", "B317", "B318", "B319", "B320", "B410", "B404", "B603"]
|
|
38
|
+
|
|
39
|
+
[tool.coverage.report]
|
|
40
|
+
include = ["ims/fieldupdater/*"]
|
|
41
|
+
omit = ["venv", "*/test*", "*upgrades.py"]
|
|
42
|
+
[tool.hatch.version]
|
|
43
|
+
path = "ims/fieldupdater/__init__.py"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build]
|
|
46
|
+
strict-naming = false
|
|
47
|
+
packages = ["ims"]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.envs.default.scripts]
|
|
50
|
+
test = "pytest {args:tests}"
|
|
51
|
+
test-cov = "coverage run -m pytest {args:tests}"
|
|
52
|
+
cov-report = "coverage html"
|
|
53
|
+
cov = [
|
|
54
|
+
"test-cov",
|
|
55
|
+
"cov-report",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.towncrier]
|
|
59
|
+
directory = "changes"
|
|
60
|
+
filename = "CHANGES.md"
|
|
61
|
+
package = "ims.fieldupdater"
|
|
62
|
+
issue_format = "[{issue}]: https://squishlist.com/ims/plone/{issue}"
|
|
63
|
+
|
|
64
|
+
[tool.towncrier.fragment.feature]
|
|
65
|
+
[tool.towncrier.fragment.bugfix]
|
|
66
|
+
[tool.towncrier.fragment.docs]
|
|
67
|
+
[tool.towncrier.fragment.breaking]
|
|
68
|
+
name = "Major Change"
|
|
69
|
+
check = true
|
|
70
|
+
|
|
71
|
+
[tool.towncrier.fragment.chore]
|
|
72
|
+
name = "Other tasks"
|
|
73
|
+
showcontent = false
|
|
74
|
+
check = false
|
|
75
|
+
|
|
76
|
+
[tool.ruff]
|
|
77
|
+
target-version = "py311"
|
|
78
|
+
line-length = 120
|
|
79
|
+
fix = true
|
|
80
|
+
lint.select = [
|
|
81
|
+
# flake8-2020
|
|
82
|
+
"YTT",
|
|
83
|
+
# flake8-bandit
|
|
84
|
+
"S",
|
|
85
|
+
# flake8-bugbear
|
|
86
|
+
"B",
|
|
87
|
+
# flake8-builtins
|
|
88
|
+
"A",
|
|
89
|
+
# flake8-comprehensions
|
|
90
|
+
"C4",
|
|
91
|
+
# flake8-debugger
|
|
92
|
+
"T10",
|
|
93
|
+
# flake8-simplify
|
|
94
|
+
"SIM",
|
|
95
|
+
# mccabe
|
|
96
|
+
"C90",
|
|
97
|
+
# pycodestyle
|
|
98
|
+
"E", "W",
|
|
99
|
+
# pyflakes
|
|
100
|
+
"F",
|
|
101
|
+
# pygrep-hooks
|
|
102
|
+
"PGH",
|
|
103
|
+
# pyupgrade
|
|
104
|
+
"UP",
|
|
105
|
+
# ruff
|
|
106
|
+
"RUF",
|
|
107
|
+
]
|
|
108
|
+
lint.ignore = [
|
|
109
|
+
# DoNotAssignLambda
|
|
110
|
+
"E731",
|
|
111
|
+
"S602",
|
|
112
|
+
"S314",
|
|
113
|
+
"S324",
|
|
114
|
+
"SIM105"
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
[tool.ruff.format]
|
|
118
|
+
preview = true
|
|
119
|
+
|
|
120
|
+
[tool.ruff.lint.per-file-ignores]
|
|
121
|
+
"tests/*" = ["S101"]
|
|
122
|
+
|
|
123
|
+
[tool.pytest.ini_options]
|
|
124
|
+
testpaths = ["tests"]
|
|
125
|
+
filterwarnings = [
|
|
126
|
+
"ignore::DeprecationWarning:Products.*",
|
|
127
|
+
"ignore::DeprecationWarning:plone.*",
|
|
128
|
+
"ignore::DeprecationWarning:z3c.*",
|
|
129
|
+
"ignore::DeprecationWarning:zope.*",
|
|
130
|
+
"ignore::DeprecationWarning:webob.*",
|
|
131
|
+
"ignore::DeprecationWarning:pkg_resources.*",
|
|
132
|
+
]
|
ims.fieldupdater-3.0.0/README.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# ims.fieldupdater
|
|
2
|
-
|
|
3
|
-
This project was inspired by Products.KeywordManager and applies some of its ideas to replacing schema field values
|
|
4
|
-
on content. Unlike that package, this one is based on schema field and not KeywordIndex. This allows you to change
|
|
5
|
-
different kinds of values whether or not they are indexed, but it is also a more expensive process.
|
|
6
|
-
|
|
7
|
-
## Process
|
|
8
|
-
- Select a schema. This is an object_provides value and/or a registered Dexterity behavior
|
|
9
|
-
- Select a field
|
|
10
|
-
- (DataGridField only - select one of the DictRow keys)
|
|
11
|
-
- Select one of the existing values on the site to constitute a match
|
|
12
|
-
- Enter a new value to replace the match (if replacing)
|
|
13
|
-
- Delete or replace
|
|
14
|
-
|
|
15
|
-
## The power of zope.schema
|
|
16
|
-
|
|
17
|
-
One of the nice things about zope.schema is that once we know which field
|
|
18
|
-
we are changing, we can look it up in the schema and use it for
|
|
19
|
-
- rendering an input widget
|
|
20
|
-
- data extraction/parsing
|
|
21
|
-
- validation
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import ims.fieldupdater
|
|
2
|
-
from plone.app.testing import PloneSandboxLayer, IntegrationTesting, FunctionalTesting, applyProfile, PLONE_FIXTURE
|
|
3
|
-
|
|
4
|
-
has_dgf = True
|
|
5
|
-
try:
|
|
6
|
-
import collective.z3cform.datagridfield
|
|
7
|
-
except ImportError:
|
|
8
|
-
has_dgf = False
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class FieldUpdaterSiteLayer(PloneSandboxLayer):
|
|
12
|
-
defaultBases = (PLONE_FIXTURE,)
|
|
13
|
-
|
|
14
|
-
def setUpZope(self, app, configuration_context):
|
|
15
|
-
# Load any other ZCML that is required for your tests.
|
|
16
|
-
# The z3c.autoinclude feature is disabled in the Plone fixture base
|
|
17
|
-
# layer.
|
|
18
|
-
if has_dgf:
|
|
19
|
-
self.loadZCML(package=collective.z3cform.datagridfield)
|
|
20
|
-
self.loadZCML(package=ims.fieldupdater)
|
|
21
|
-
|
|
22
|
-
def setUpPloneSite(self, portal):
|
|
23
|
-
if has_dgf:
|
|
24
|
-
applyProfile(portal, 'collective.z3cform.datagridfield:default')
|
|
25
|
-
applyProfile(portal, 'ims.fieldupdater:default')
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
FIELD_UPDATER_SITE_FIXTURE = FieldUpdaterSiteLayer()
|
|
29
|
-
|
|
30
|
-
INTEGRATION = IntegrationTesting(
|
|
31
|
-
bases=(FIELD_UPDATER_SITE_FIXTURE,),
|
|
32
|
-
name="ims.fieldupdater:Integration"
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
FUNCTIONAL = FunctionalTesting(
|
|
36
|
-
bases=(FIELD_UPDATER_SITE_FIXTURE,),
|
|
37
|
-
name="ims.fieldupdater:Functional"
|
|
38
|
-
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
#
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
|
|
3
|
-
import transaction
|
|
4
|
-
from plone.app.testing import setRoles, TEST_USER_ID, SITE_OWNER_NAME, SITE_OWNER_PASSWORD
|
|
5
|
-
from plone.testing.zope import Browser
|
|
6
|
-
from zope.interface.declarations import directlyProvides
|
|
7
|
-
|
|
8
|
-
from .interfaces import IMassEditTest
|
|
9
|
-
from .. import testing
|
|
10
|
-
|
|
11
|
-
try:
|
|
12
|
-
from Products.CMFCore.indexing import processQueue
|
|
13
|
-
except ImportError:
|
|
14
|
-
def processQueue():
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class UnitTestCase(unittest.TestCase):
|
|
19
|
-
def setUp(self):
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class IntegrationTestCase(unittest.TestCase):
|
|
24
|
-
layer = testing.INTEGRATION
|
|
25
|
-
|
|
26
|
-
def setUp(self):
|
|
27
|
-
super(IntegrationTestCase, self).setUp()
|
|
28
|
-
self.portal = self.layer['portal']
|
|
29
|
-
self.request = self.layer
|
|
30
|
-
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
|
31
|
-
self.view = self.portal.restrictedTraverse('@@mass-edit')
|
|
32
|
-
self.portal.invokeFactory('Document', 'page1')
|
|
33
|
-
self.portal.invokeFactory('Document', 'page2')
|
|
34
|
-
|
|
35
|
-
self.page1 = self.portal['page1']
|
|
36
|
-
self.page2 = self.portal['page2']
|
|
37
|
-
directlyProvides(self.page1, IMassEditTest)
|
|
38
|
-
directlyProvides(self.page2, IMassEditTest)
|
|
39
|
-
self.page1.reindexObject()
|
|
40
|
-
self.page2.reindexObject()
|
|
41
|
-
processQueue()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class FunctionalTestCase(IntegrationTestCase):
|
|
45
|
-
layer = testing.FUNCTIONAL
|
|
46
|
-
|
|
47
|
-
def setUp(self):
|
|
48
|
-
super(FunctionalTestCase, self).setUp()
|
|
49
|
-
self.browser = Browser(self.layer['app'])
|
|
50
|
-
self.browser.handleErrors = False
|
|
51
|
-
self.browser.addHeader(
|
|
52
|
-
'Authorization',
|
|
53
|
-
'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD,)
|
|
54
|
-
)
|
|
55
|
-
transaction.commit()
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
from zope.interface import Interface
|
|
2
|
-
from zope.schema import List, TextLine, Datetime, Date, Choice
|
|
3
|
-
from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
|
|
4
|
-
|
|
5
|
-
list_field_opts = SimpleVocabulary([
|
|
6
|
-
SimpleTerm(value='fermi', title='Fermi'),
|
|
7
|
-
SimpleTerm(value='einstein', title='Einstein'),
|
|
8
|
-
SimpleTerm(value='bohr', title='Bohr'),
|
|
9
|
-
SimpleTerm(value='heisenberg', title='Heisenberg'),
|
|
10
|
-
SimpleTerm(value='hawking', title='Hawking'),
|
|
11
|
-
])
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class IMassEditTest(Interface):
|
|
15
|
-
list_field = List(
|
|
16
|
-
title='List field',
|
|
17
|
-
value_type=TextLine(),
|
|
18
|
-
)
|
|
19
|
-
list_choice_field = List(
|
|
20
|
-
title='List choice field',
|
|
21
|
-
value_type=Choice(
|
|
22
|
-
vocabulary=list_field_opts,
|
|
23
|
-
)
|
|
24
|
-
)
|
|
25
|
-
text_field = TextLine(
|
|
26
|
-
title='Text field',
|
|
27
|
-
required=False,
|
|
28
|
-
)
|
|
29
|
-
text_field_required = TextLine(
|
|
30
|
-
title='Text field',
|
|
31
|
-
required=True
|
|
32
|
-
)
|
|
33
|
-
date_time_field = Datetime(
|
|
34
|
-
title='Datetime field',
|
|
35
|
-
)
|
|
36
|
-
date_field = Date(
|
|
37
|
-
title='Date field',
|
|
38
|
-
)
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
from zope.schema.interfaces import RequiredMissing
|
|
2
|
-
|
|
3
|
-
from . import base
|
|
4
|
-
from .interfaces import IMassEditTest
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestMassIntegration(base.IntegrationTestCase):
|
|
8
|
-
def test_schemas(self):
|
|
9
|
-
self.assertIn({'id': IMassEditTest.__identifier__, 'title': 'IMassEditTest'}, self.view.get_schemas())
|
|
10
|
-
|
|
11
|
-
def test_list_replace(self):
|
|
12
|
-
self.page1.list_field = ['einstein', 'bohr']
|
|
13
|
-
self.page2.list_field = ['fermi', 'heisenberg']
|
|
14
|
-
match = 'einstein'
|
|
15
|
-
field = 'list_field'
|
|
16
|
-
replacement = 'hawking'
|
|
17
|
-
schema = IMassEditTest.__identifier__
|
|
18
|
-
self.view.request['schema'] = schema
|
|
19
|
-
self.view.request['field'] = field
|
|
20
|
-
self.view.request['match'] = match
|
|
21
|
-
self.view.request['replacement'] = replacement
|
|
22
|
-
self.view.replace_term(schema, field, None, match)
|
|
23
|
-
self.assertEqual(self.page1.list_field, ['hawking', 'bohr'])
|
|
24
|
-
self.assertEqual(self.page2.list_field, ['fermi', 'heisenberg']) # sanity - no change
|
|
25
|
-
|
|
26
|
-
def test_list_delete(self):
|
|
27
|
-
self.page1.list_field = ['einstein', 'bohr']
|
|
28
|
-
self.page2.list_field = ['fermi', 'heisenberg']
|
|
29
|
-
match = 'einstein'
|
|
30
|
-
field = 'list_field'
|
|
31
|
-
schema = IMassEditTest.__identifier__
|
|
32
|
-
self.view.request['schema'] = schema
|
|
33
|
-
self.view.request['field'] = field
|
|
34
|
-
self.view.request['match'] = match
|
|
35
|
-
self.view.delete_term(schema, field, None, match)
|
|
36
|
-
self.assertEqual(self.page1.list_field, ['bohr'])
|
|
37
|
-
self.assertEqual(self.page2.list_field, ['fermi', 'heisenberg']) # sanity - no change
|
|
38
|
-
|
|
39
|
-
def test_list_replace_wrong_type(self):
|
|
40
|
-
""" str converted to unicode """
|
|
41
|
-
self.page1.list_field = ['einstein', 'bohr']
|
|
42
|
-
self.page2.list_field = ['fermi', 'heisenberg']
|
|
43
|
-
match = 'einstein'
|
|
44
|
-
field = 'list_field'
|
|
45
|
-
replacement = 'hawking'
|
|
46
|
-
schema = IMassEditTest.__identifier__
|
|
47
|
-
self.view.request['schema'] = schema
|
|
48
|
-
self.view.request['field'] = field
|
|
49
|
-
self.view.request['match'] = match
|
|
50
|
-
self.view.request['replacement'] = replacement
|
|
51
|
-
self.view.replace_term(schema, field, None, match)
|
|
52
|
-
self.assertEqual(self.page1.list_field, ['hawking', 'bohr'])
|
|
53
|
-
|
|
54
|
-
def test_list_choice_replace(self):
|
|
55
|
-
""" A term outside of vocab will come in as NO_VALUE and result in no change """
|
|
56
|
-
self.page1.list_choice_field = ['einstein', 'bohr']
|
|
57
|
-
self.page2.list_choice_field = ['fermi', 'heisenberg']
|
|
58
|
-
match = 'einstein'
|
|
59
|
-
field = 'list_choice_field'
|
|
60
|
-
replacement = 'hawking'
|
|
61
|
-
schema = IMassEditTest.__identifier__
|
|
62
|
-
self.view.request['schema'] = schema
|
|
63
|
-
self.view.request['field'] = field
|
|
64
|
-
self.view.request['match'] = match
|
|
65
|
-
self.view.request['replacement'] = replacement
|
|
66
|
-
self.view.replace_term(schema, field, None, match)
|
|
67
|
-
self.assertEqual(self.page1.list_choice_field, ['hawking', 'bohr'])
|
|
68
|
-
self.assertEqual(self.page2.list_choice_field, ['fermi', 'heisenberg']) # sanity - no change
|
|
69
|
-
|
|
70
|
-
def test_list_choice_replace_invalid(self):
|
|
71
|
-
self.page1.list_choice_field = ['einstein', 'bohr']
|
|
72
|
-
self.page2.list_choice_field = ['fermi', 'heisenberg']
|
|
73
|
-
match = 'einstein'
|
|
74
|
-
field = 'list_choice_field'
|
|
75
|
-
replacement = 'dirac'
|
|
76
|
-
schema = IMassEditTest.__identifier__
|
|
77
|
-
self.view.request['schema'] = schema
|
|
78
|
-
self.view.request['field'] = field
|
|
79
|
-
self.view.request['match'] = match
|
|
80
|
-
self.view.request['replacement'] = replacement
|
|
81
|
-
self.view.replace_term(schema, field, None, match)
|
|
82
|
-
self.assertNotIn('dirac', self.page1.list_choice_field)
|
|
83
|
-
|
|
84
|
-
def test_textline_replace(self):
|
|
85
|
-
self.page1.text_field = 'einstein'
|
|
86
|
-
match = 'einstein'
|
|
87
|
-
field = 'text_field'
|
|
88
|
-
replacement = 'hawking'
|
|
89
|
-
schema = IMassEditTest.__identifier__
|
|
90
|
-
self.view.request['schema'] = schema
|
|
91
|
-
self.view.request['field'] = field
|
|
92
|
-
self.view.request['match'] = match
|
|
93
|
-
self.view.request['replacement'] = replacement
|
|
94
|
-
self.view.replace_term(schema, field, None, match)
|
|
95
|
-
self.assertEqual(self.page1.text_field, 'hawking')
|
|
96
|
-
|
|
97
|
-
def test_textline_delete(self):
|
|
98
|
-
self.page1.text_field = 'einstein'
|
|
99
|
-
match = 'einstein'
|
|
100
|
-
field = 'text_field'
|
|
101
|
-
schema = IMassEditTest.__identifier__
|
|
102
|
-
self.view.request['schema'] = schema
|
|
103
|
-
self.view.request['field'] = field
|
|
104
|
-
self.view.request['match'] = match
|
|
105
|
-
self.view.delete_term(schema, field, None, match)
|
|
106
|
-
self.assertEqual(self.page1.text_field, None)
|
|
107
|
-
|
|
108
|
-
def test_textline_delete_required(self):
|
|
109
|
-
self.page1.text_field_required = 'einstein'
|
|
110
|
-
match = 'einstein'
|
|
111
|
-
field = 'text_field_required'
|
|
112
|
-
schema = IMassEditTest.__identifier__
|
|
113
|
-
self.view.request['schema'] = schema
|
|
114
|
-
self.view.request['field'] = field
|
|
115
|
-
self.view.request['match'] = match
|
|
116
|
-
self.assertRaises(RequiredMissing, self.view.delete_term, schema, field, None, match)
|
|
117
|
-
|
|
118
|
-
def test_unicode_conversion(self):
|
|
119
|
-
""" The widget should really handle this, but we do have this as a failsafe """
|
|
120
|
-
self.page1.text_field = 'einstein'
|
|
121
|
-
match = 'einstein'
|
|
122
|
-
field = 'text_field'
|
|
123
|
-
replacement = 'hawking'
|
|
124
|
-
schema = IMassEditTest.__identifier__
|
|
125
|
-
self.view.request['schema'] = schema
|
|
126
|
-
self.view.request['field'] = field
|
|
127
|
-
self.view.request['match'] = match
|
|
128
|
-
self.view.request['replacement'] = replacement
|
|
129
|
-
self.view.replace_term(schema, field, None, match)
|
|
130
|
-
self.assertEqual(self.page1.text_field, 'hawking')
|
|
131
|
-
self.assertIsInstance(self.page1.text_field, str)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
class TestMassFunctional(base.FunctionalTestCase):
|
|
135
|
-
def test_mass_edit(self):
|
|
136
|
-
self.page1.list_choice_field = 'einstein'
|
|
137
|
-
self.browser.open(self.portal.absolute_url() + '/@@mass-edit?schema=' +
|
|
138
|
-
IMassEditTest.__identifier__ + '&field=list_choice_field&match=')
|
|
139
|
-
# ctrl = self.browser.getControl
|
|
140
|
-
# ctrl(name='form.buttons.search').click()
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def test_suite():
|
|
144
|
-
import unittest
|
|
145
|
-
return unittest.defaultTestLoader.loadTestsFromName(__name__)
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from Products.CMFPlone.utils import get_installer
|
|
2
|
-
from plone import api
|
|
3
|
-
from plone.app.testing import TEST_USER_ID
|
|
4
|
-
from plone.app.testing import setRoles
|
|
5
|
-
|
|
6
|
-
from . import base
|
|
7
|
-
from .. import testing
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class TestUninstall(base.IntegrationTestCase):
|
|
11
|
-
layer = testing.INTEGRATION
|
|
12
|
-
|
|
13
|
-
def setUp(self):
|
|
14
|
-
self.portal = self.layer['portal']
|
|
15
|
-
roles_before = api.user.get_roles(TEST_USER_ID)
|
|
16
|
-
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
|
17
|
-
self.installer = get_installer(self.portal)
|
|
18
|
-
self.installer.uninstall_product('ims.fieldupdater')
|
|
19
|
-
setRoles(self.portal, TEST_USER_ID, roles_before)
|
|
20
|
-
|
|
21
|
-
def test_product_uninstalled(self):
|
|
22
|
-
"""Test if ims.contacts is cleanly uninstalled."""
|
|
23
|
-
self.assertFalse(self.installer.is_product_installed('ims.fieldupdater'))
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: ims.fieldupdater
|
|
3
|
-
Version: 3.0.0
|
|
4
|
-
Summary: Update all objects in Plone based on a schema/field strategy
|
|
5
|
-
Author-email: Eric Wohnlich <wohnlice@imsweb.com>
|
|
6
|
-
License: GPL
|
|
7
|
-
Project-URL: homepage, https://git.imsweb.com/plone/ims.fieldupdater
|
|
8
|
-
Project-URL: documentation, https://git.imsweb.com/plone/ims.fieldupdater
|
|
9
|
-
Project-URL: repository, https://git.imsweb.com/plone/ims.fieldupdater
|
|
10
|
-
Classifier: Framework :: Plone :: 6.0
|
|
11
|
-
Classifier: Programming Language :: Python
|
|
12
|
-
Requires-Python: >=3.8
|
|
13
|
-
Requires-Dist: plone>=6.0.4
|
|
14
|
-
Provides-Extra: test
|
|
15
|
-
Requires-Dist: plone.app.testing; extra == "test"
|
|
16
|
-
Requires-Dist: plone.mocktestcase; extra == "test"
|
|
17
|
-
Requires-Dist: formencode; extra == "test"
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
MANIFEST.in
|
|
2
|
-
README.md
|
|
3
|
-
pyproject.toml
|
|
4
|
-
setup.cfg
|
|
5
|
-
ims.fieldupdater.egg-info/PKG-INFO
|
|
6
|
-
ims.fieldupdater.egg-info/SOURCES.txt
|
|
7
|
-
ims.fieldupdater.egg-info/dependency_links.txt
|
|
8
|
-
ims.fieldupdater.egg-info/entry_points.txt
|
|
9
|
-
ims.fieldupdater.egg-info/requires.txt
|
|
10
|
-
ims.fieldupdater.egg-info/top_level.txt
|
|
11
|
-
ims/fieldupdater/__init__.py
|
|
12
|
-
ims/fieldupdater/configure.zcml
|
|
13
|
-
ims/fieldupdater/testing.py
|
|
14
|
-
ims/fieldupdater/browser/__init__.py
|
|
15
|
-
ims/fieldupdater/browser/configure.zcml
|
|
16
|
-
ims/fieldupdater/browser/mass.pt
|
|
17
|
-
ims/fieldupdater/browser/mass.py
|
|
18
|
-
ims/fieldupdater/browser/schema.pt
|
|
19
|
-
ims/fieldupdater/profiles/default/controlpanel.xml
|
|
20
|
-
ims/fieldupdater/profiles/default/metadata.xml
|
|
21
|
-
ims/fieldupdater/profiles/uninstall/controlpanel.xml
|
|
22
|
-
ims/fieldupdater/tests/__init__.py
|
|
23
|
-
ims/fieldupdater/tests/base.py
|
|
24
|
-
ims/fieldupdater/tests/interfaces.py
|
|
25
|
-
ims/fieldupdater/tests/test_mass.py
|
|
26
|
-
ims/fieldupdater/tests/test_uninstall.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
ims
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools", "wheel"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "ims.fieldupdater"
|
|
7
|
-
version = "3.0.0"
|
|
8
|
-
requires-python = ">=3.8"
|
|
9
|
-
description = "Update all objects in Plone based on a schema/field strategy"
|
|
10
|
-
classifiers = [
|
|
11
|
-
"Framework :: Plone :: 6.0",
|
|
12
|
-
"Programming Language :: Python",
|
|
13
|
-
]
|
|
14
|
-
authors = [
|
|
15
|
-
{ name = "Eric Wohnlich", email = "wohnlice@imsweb.com" }
|
|
16
|
-
]
|
|
17
|
-
license = { text = "GPL" }
|
|
18
|
-
|
|
19
|
-
dependencies = [
|
|
20
|
-
"plone>=6.0.4"
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
[project.urls]
|
|
24
|
-
homepage = "https://git.imsweb.com/plone/ims.fieldupdater"
|
|
25
|
-
documentation = "https://git.imsweb.com/plone/ims.fieldupdater"
|
|
26
|
-
repository = "https://git.imsweb.com/plone/ims.fieldupdater"
|
|
27
|
-
|
|
28
|
-
[project.optional-dependencies]
|
|
29
|
-
test = ["plone.app.testing", "plone.mocktestcase", "formencode"]
|
|
30
|
-
|
|
31
|
-
[project.entry-points."plone.autoinclude.plugin"]
|
|
32
|
-
target = "plone"
|
|
33
|
-
|
|
34
|
-
[tool.bandit]
|
|
35
|
-
exclude_dirs = ["*/tests/*"]
|
|
36
|
-
skips = ["B313", "B314", "B315", "B316", "B317", "B318", "B319", "B320", "B410", "B404", "B603"]
|
|
37
|
-
|
|
38
|
-
[tool.coverage.report]
|
|
39
|
-
include = ["ims/fieldupdater/*"]
|
|
40
|
-
omit = ["venv", "*/test*", "*upgrades.py"]
|
|
41
|
-
|
|
42
|
-
[tool.setuptools.packages.find]
|
|
43
|
-
include = ["ims"]
|
|
44
|
-
|
|
45
|
-
[tool.flake8]
|
|
46
|
-
max-line-length = 120
|
ims.fieldupdater-3.0.0/setup.cfg
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/ims/fieldupdater/profiles/default/controlpanel.xml
RENAMED
|
File without changes
|
{ims.fieldupdater-3.0.0 → ims.fieldupdater-3.0.1}/ims/fieldupdater/profiles/default/metadata.xml
RENAMED
|
File without changes
|
|
File without changes
|