browsergym-workarena 0.1.0rc6__py3-none-any.whl → 0.2.0__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.
- browsergym/workarena/__init__.py +7 -2
- browsergym/workarena/api/ui_themes.py +35 -0
- browsergym/workarena/api/user.py +153 -0
- browsergym/workarena/api/utils.py +1 -1
- browsergym/workarena/config.py +43 -1
- browsergym/workarena/data_files/setup_files/lists/expected_asset_list_columns.json +34 -1
- browsergym/workarena/data_files/setup_files/lists/expected_change_request_list_columns.json +48 -1
- browsergym/workarena/data_files/setup_files/lists/expected_hardware_list_columns.json +53 -1
- browsergym/workarena/data_files/setup_files/lists/expected_incident_list_columns.json +28 -1
- browsergym/workarena/data_files/setup_files/lists/expected_service_catalog_list_columns.json +29 -1
- browsergym/workarena/data_files/setup_files/ui_themes/workarena_themes.xml +2313 -0
- browsergym/workarena/data_files/task_configs/dashboard_retrieval_minmax_task.json +1 -0
- browsergym/workarena/data_files/task_configs/dashboard_retrieval_value_task.json +1 -0
- browsergym/workarena/data_files/task_configs/report_retrieval_minmax_task.json +1 -0
- browsergym/workarena/data_files/task_configs/report_retrieval_value_task.json +1 -0
- browsergym/workarena/data_files/task_configs/sort_asset_list_task.json +547 -11391
- browsergym/workarena/data_files/task_configs/sort_change_request_list_task.json +558 -11090
- browsergym/workarena/data_files/task_configs/sort_hardware_list_task.json +576 -11162
- browsergym/workarena/data_files/task_configs/sort_incident_list_task.json +528 -11172
- browsergym/workarena/data_files/task_configs/sort_service_catalog_item_list_task.json +533 -11491
- browsergym/workarena/data_files/task_configs/sort_user_list_task.json +568 -10582
- browsergym/workarena/install.py +625 -153
- browsergym/workarena/tasks/base.py +85 -26
- browsergym/workarena/tasks/dashboard.py +620 -0
- browsergym/workarena/tasks/form.py +127 -90
- browsergym/workarena/tasks/knowledge.py +30 -14
- browsergym/workarena/tasks/list.py +157 -65
- browsergym/workarena/tasks/navigation.py +18 -16
- browsergym/workarena/tasks/scripts/generate_dashboard_configs.py +272 -0
- browsergym/workarena/tasks/scripts/generate_forms.py +2 -2
- browsergym/workarena/tasks/scripts/list.py +33 -9
- browsergym/workarena/tasks/scripts/validate.py +2 -2
- browsergym/workarena/tasks/service_catalog.py +106 -74
- browsergym/workarena/tasks/utils/form.py +5 -3
- browsergym/workarena/tasks/utils/js_utils.js +123 -2
- browsergym/workarena/tasks/utils/string.py +15 -0
- browsergym/workarena/tasks/utils/utils.py +20 -0
- browsergym/workarena/utils.py +31 -2
- {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/METADATA +7 -3
- {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/RECORD +43 -32
- {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/WHEEL +1 -1
- {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/entry_points.txt +0 -0
- {browsergym_workarena-0.1.0rc6.dist-info → browsergym_workarena-0.2.0.dist-info}/licenses/LICENSE +0 -0
browsergym/workarena/install.py
CHANGED
|
@@ -2,11 +2,17 @@ import html
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
5
|
+
import tenacity
|
|
5
6
|
|
|
7
|
+
from datetime import datetime
|
|
6
8
|
from playwright.sync_api import sync_playwright
|
|
7
9
|
from tenacity import retry, stop_after_attempt, retry_if_exception_type
|
|
10
|
+
from requests import HTTPError
|
|
11
|
+
from time import sleep
|
|
8
12
|
|
|
9
|
-
from .api.
|
|
13
|
+
from .api.ui_themes import get_workarena_theme_variants
|
|
14
|
+
from .api.user import create_user
|
|
15
|
+
from .api.utils import table_api_call, table_column_info
|
|
10
16
|
from .config import (
|
|
11
17
|
# for knowledge base setup
|
|
12
18
|
KB_FILEPATH,
|
|
@@ -24,16 +30,140 @@ from .config import (
|
|
|
24
30
|
EXPECTED_INCIDENT_FORM_FIELDS_PATH,
|
|
25
31
|
EXPECTED_PROBLEM_FORM_FIELDS_PATH,
|
|
26
32
|
EXPECTED_USER_FORM_FIELDS_PATH,
|
|
33
|
+
# Patch flag for reports
|
|
34
|
+
REPORT_PATCH_FLAG,
|
|
35
|
+
REPORT_DATE_FILTER,
|
|
36
|
+
# Supported ServiceNow releases
|
|
37
|
+
SNOW_SUPPORTED_RELEASES,
|
|
27
38
|
# For workflows setup
|
|
28
39
|
WORKFLOWS,
|
|
40
|
+
# For UI themes setup
|
|
41
|
+
UI_THEMES_UPDATE_SET,
|
|
29
42
|
)
|
|
43
|
+
from .api.user import set_user_preference
|
|
30
44
|
from .instance import SNowInstance
|
|
31
|
-
from .utils import
|
|
45
|
+
from .utils import url_login
|
|
32
46
|
|
|
33
47
|
|
|
34
|
-
def
|
|
48
|
+
def _set_sys_property(property_name: str, value: str):
|
|
49
|
+
"""
|
|
50
|
+
Set a sys_property in the instance.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
instance = SNowInstance()
|
|
54
|
+
|
|
55
|
+
property = table_api_call(
|
|
56
|
+
instance=instance,
|
|
57
|
+
table="sys_properties",
|
|
58
|
+
params={"sysparm_query": f"name={property_name}", "sysparm_fields": "sys_id"},
|
|
59
|
+
)["result"]
|
|
60
|
+
|
|
61
|
+
if not property:
|
|
62
|
+
property_sysid = ""
|
|
63
|
+
method = "POST"
|
|
64
|
+
else:
|
|
65
|
+
property_sysid = "/" + property[0]["sys_id"]
|
|
66
|
+
method = "PUT"
|
|
67
|
+
|
|
68
|
+
property = table_api_call(
|
|
69
|
+
instance=instance,
|
|
70
|
+
table=f"sys_properties{property_sysid}",
|
|
71
|
+
method=method,
|
|
72
|
+
json={"name": property_name, "value": value},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Verify that the property was updated
|
|
76
|
+
assert property["result"]["value"] == value, f"Error setting {property_name}."
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get_sys_property(property_name: str) -> str:
|
|
80
|
+
"""
|
|
81
|
+
Get a sys_property from the instance.
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
instance = SNowInstance()
|
|
85
|
+
|
|
86
|
+
property_value = table_api_call(
|
|
87
|
+
instance=instance,
|
|
88
|
+
table="sys_properties",
|
|
89
|
+
params={"sysparm_query": f"name={property_name}", "sysparm_fields": "value"},
|
|
90
|
+
)["result"][0]["value"]
|
|
91
|
+
|
|
92
|
+
return property_value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _install_update_set(path: str, name: str):
|
|
96
|
+
"""
|
|
97
|
+
Install a ServiceNow update set
|
|
98
|
+
|
|
99
|
+
Parameters:
|
|
100
|
+
-----------
|
|
101
|
+
path: str
|
|
102
|
+
The path to the update set file.
|
|
103
|
+
name: str
|
|
104
|
+
The name of the update set as it should appear in the UI.
|
|
105
|
+
|
|
106
|
+
Notes: requires interacting with the UI, so we use playwright instead of the API
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
with sync_playwright() as playwright:
|
|
110
|
+
instance = SNowInstance()
|
|
111
|
+
browser = playwright.chromium.launch(headless=True, slow_mo=1000)
|
|
112
|
+
page = browser.new_page()
|
|
113
|
+
url_login(instance, page)
|
|
114
|
+
|
|
115
|
+
# Navigate to the update set upload page and upload all update sets
|
|
116
|
+
logging.info("Uploading update set...")
|
|
117
|
+
page.goto(
|
|
118
|
+
instance.snow_url
|
|
119
|
+
+ "/now/nav/ui/classic/params/target/upload.do%3Fsysparm_referring_url%3Dsys_remote_update_set_list.do%253Fsysparm_fixed_query%253Dsys_class_name%253Dsys_remote_update_set%26sysparm_target%3Dsys_remote_update_set"
|
|
120
|
+
)
|
|
121
|
+
iframe = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
|
|
122
|
+
with page.expect_file_chooser() as fc_info:
|
|
123
|
+
iframe.locator("#attachFile").click()
|
|
124
|
+
file_chooser = fc_info.value
|
|
125
|
+
file_chooser.set_files(path)
|
|
126
|
+
iframe.locator("input:text('Upload')").click()
|
|
127
|
+
sleep(5)
|
|
128
|
+
|
|
129
|
+
# Apply all update sets
|
|
130
|
+
logging.info("Applying update set...")
|
|
131
|
+
# ... retrieve all update sets that are ready to be applied
|
|
132
|
+
update_set = table_api_call(
|
|
133
|
+
instance=instance,
|
|
134
|
+
table="sys_remote_update_set",
|
|
135
|
+
params={
|
|
136
|
+
"sysparm_query": f"name={name}^state=loaded",
|
|
137
|
+
},
|
|
138
|
+
)["result"][0]
|
|
139
|
+
# ... apply them
|
|
140
|
+
logging.info(f"... {update_set['name']}")
|
|
141
|
+
page.goto(instance.snow_url + "/sys_remote_update_set.do?sys_id=" + update_set["sys_id"])
|
|
142
|
+
page.locator("button:has-text('Preview Update Set')").first.click()
|
|
143
|
+
page.wait_for_selector("text=Succeeded")
|
|
144
|
+
# click escape to close popup
|
|
145
|
+
page.keyboard.press("Escape")
|
|
146
|
+
page.locator("button:has-text('Commit Update Set')").first.click()
|
|
147
|
+
page.wait_for_selector("text=Succeeded")
|
|
148
|
+
|
|
149
|
+
browser.close()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def check_knowledge_base(
|
|
153
|
+
instance: SNowInstance, kb_name: str, kb_data: dict, disable_commenting: bool = True
|
|
154
|
+
):
|
|
35
155
|
"""
|
|
36
156
|
Verify the integrity of the knowledge base in the instance.
|
|
157
|
+
Args:
|
|
158
|
+
-----
|
|
159
|
+
instance: SNowInstance
|
|
160
|
+
The ServiceNow instance to check the knowledge base in
|
|
161
|
+
kb_name: str
|
|
162
|
+
The name of the knowledge base to check
|
|
163
|
+
kb_data: dict
|
|
164
|
+
The knowledge base data to check
|
|
165
|
+
disable_commenting: bool
|
|
166
|
+
Whether to disable commenting on the knowledge base
|
|
37
167
|
|
|
38
168
|
"""
|
|
39
169
|
|
|
@@ -47,7 +177,7 @@ def check_knowledge_base(instance: SNowInstance, kb_data: dict):
|
|
|
47
177
|
kb = table_api_call(
|
|
48
178
|
instance=instance,
|
|
49
179
|
table="kb_knowledge_base",
|
|
50
|
-
params={"sysparm_query": f"title={
|
|
180
|
+
params={"sysparm_query": f"title={kb_name}"},
|
|
51
181
|
)["result"]
|
|
52
182
|
|
|
53
183
|
# The KB exists
|
|
@@ -55,6 +185,17 @@ def check_knowledge_base(instance: SNowInstance, kb_data: dict):
|
|
|
55
185
|
requires_install = False
|
|
56
186
|
requires_delete = False
|
|
57
187
|
|
|
188
|
+
# Check that the KB has the correct settings
|
|
189
|
+
if disable_commenting and (
|
|
190
|
+
kb[0]["disable_commenting"] != "true"
|
|
191
|
+
or kb[0]["disable_mark_as_helpful"] != "true"
|
|
192
|
+
or kb[0]["disable_rating"] != "true"
|
|
193
|
+
or kb[0]["disable_suggesting"] != "true"
|
|
194
|
+
or kb[0]["disable_category_editing"] != "true"
|
|
195
|
+
):
|
|
196
|
+
requires_install = True
|
|
197
|
+
requires_delete = True
|
|
198
|
+
|
|
58
199
|
# Get all articles in the KB
|
|
59
200
|
articles = table_api_call(
|
|
60
201
|
instance=instance,
|
|
@@ -73,6 +214,7 @@ def check_knowledge_base(instance: SNowInstance, kb_data: dict):
|
|
|
73
214
|
# Invalid article title, the KB is corrupt and must be reinstalled
|
|
74
215
|
requires_install = True
|
|
75
216
|
requires_delete = True
|
|
217
|
+
break
|
|
76
218
|
|
|
77
219
|
# Check that the articles match (preprocess the text because ServiceNow adds some HTML tags)
|
|
78
220
|
if _extract_text(kb_data[idx]["article"]) != _extract_text(a["text"]):
|
|
@@ -98,7 +240,7 @@ def check_knowledge_base(instance: SNowInstance, kb_data: dict):
|
|
|
98
240
|
)
|
|
99
241
|
|
|
100
242
|
|
|
101
|
-
def delete_knowledge_base(instance: SNowInstance, kb_id: str):
|
|
243
|
+
def delete_knowledge_base(instance: SNowInstance, kb_id: str, kb_name: str):
|
|
102
244
|
"""
|
|
103
245
|
Delete a knowledge base from the instance.
|
|
104
246
|
|
|
@@ -112,12 +254,12 @@ def delete_knowledge_base(instance: SNowInstance, kb_id: str):
|
|
|
112
254
|
)["result"]
|
|
113
255
|
|
|
114
256
|
# Delete the knowledge base
|
|
115
|
-
logging.info("
|
|
257
|
+
logging.info(f"Knowledge base {kb_name}: deleting knowledge base content")
|
|
116
258
|
for a_ in articles:
|
|
117
259
|
table_api_call(instance=instance, table=f"kb_knowledge/{a_['sys_id']}", method="DELETE")
|
|
118
260
|
|
|
119
261
|
# Rename the KB and set active=False (ServiceNow prevents deletion)
|
|
120
|
-
logging.info("
|
|
262
|
+
logging.info(f"Knowledge base {kb_name}: archiving knowledge base")
|
|
121
263
|
table_api_call(
|
|
122
264
|
instance=instance,
|
|
123
265
|
table=f"kb_knowledge_base/{kb_id}",
|
|
@@ -126,25 +268,48 @@ def delete_knowledge_base(instance: SNowInstance, kb_id: str):
|
|
|
126
268
|
)
|
|
127
269
|
|
|
128
270
|
|
|
129
|
-
def create_knowledge_base(
|
|
271
|
+
def create_knowledge_base(
|
|
272
|
+
instance: SNowInstance, kb_name: str, kb_data: dict, disable_commenting: bool = True
|
|
273
|
+
):
|
|
130
274
|
"""
|
|
131
|
-
Create knowledge base and upload all articles
|
|
275
|
+
Create knowledge base and upload all articles.
|
|
276
|
+
Params:
|
|
277
|
+
-------
|
|
278
|
+
instance: SNowInstance
|
|
279
|
+
The ServiceNow instance to install the knowledge base in
|
|
280
|
+
kb_name: str
|
|
281
|
+
The name of the knowledge base that will be created
|
|
282
|
+
kb_data: dict
|
|
283
|
+
The knowledge base data to upload
|
|
284
|
+
disable_commenting: bool
|
|
285
|
+
Whether to disable commenting on the knowledge base
|
|
132
286
|
|
|
133
287
|
"""
|
|
134
|
-
logging.info("Installing knowledge base...")
|
|
288
|
+
logging.info(f"Installing knowledge base {kb_name}...")
|
|
135
289
|
|
|
136
290
|
# Create the knowledge base
|
|
137
|
-
logging.info("... creating knowledge base")
|
|
291
|
+
logging.info(f"... creating knowledge base {kb_name}")
|
|
292
|
+
disable_commenting = "true" if disable_commenting else "false"
|
|
293
|
+
|
|
138
294
|
kb = table_api_call(
|
|
139
295
|
instance=instance,
|
|
140
296
|
table="kb_knowledge_base",
|
|
141
297
|
method="POST",
|
|
142
|
-
data=json.dumps(
|
|
298
|
+
data=json.dumps(
|
|
299
|
+
{
|
|
300
|
+
"title": kb_name,
|
|
301
|
+
"disable_commenting": disable_commenting,
|
|
302
|
+
"disable_mark_as_helpful": disable_commenting,
|
|
303
|
+
"disable_rating": disable_commenting,
|
|
304
|
+
"disable_suggesting": disable_commenting,
|
|
305
|
+
"disable_category_editing": disable_commenting,
|
|
306
|
+
}
|
|
307
|
+
),
|
|
143
308
|
)["result"]
|
|
144
309
|
kb_id = kb["sys_id"]
|
|
145
310
|
|
|
146
311
|
for i, kb_entry in enumerate(kb_data):
|
|
147
|
-
logging.info(f"... uploading article {i + 1}/{len(kb_data)}")
|
|
312
|
+
logging.info(f"... Knowledge Base {kb_name} uploading article {i + 1}/{len(kb_data)}")
|
|
148
313
|
article = kb_entry["article"]
|
|
149
314
|
|
|
150
315
|
# Plant a new article in kb_knowledge table
|
|
@@ -164,7 +329,7 @@ def create_knowledge_base(instance: SNowInstance, kb_data: dict):
|
|
|
164
329
|
)
|
|
165
330
|
|
|
166
331
|
|
|
167
|
-
def
|
|
332
|
+
def setup_knowledge_bases():
|
|
168
333
|
"""
|
|
169
334
|
Verify that the knowledge base is installed correctly in the instance.
|
|
170
335
|
If it is not, it will be installed.
|
|
@@ -172,33 +337,47 @@ def setup_knowledge_base():
|
|
|
172
337
|
"""
|
|
173
338
|
# Get the ServiceNow instance
|
|
174
339
|
instance = SNowInstance()
|
|
340
|
+
# Mapping between knowledge base name and filepath + whether or not to disable comments
|
|
341
|
+
knowledge_bases = {
|
|
342
|
+
KB_NAME: (KB_FILEPATH, True),
|
|
343
|
+
}
|
|
344
|
+
for kb_name, (kb_filepath, disable_commenting) in knowledge_bases.items():
|
|
345
|
+
# Load the knowledge base
|
|
346
|
+
with open(kb_filepath, "r") as f:
|
|
347
|
+
kb_data = json.load(f)
|
|
175
348
|
|
|
176
|
-
# Load the knowledge base
|
|
177
|
-
with open(KB_FILEPATH, "r") as f:
|
|
178
|
-
kb_data = json.load(f)
|
|
179
|
-
|
|
180
|
-
kb_id, requires_install, requires_delete = check_knowledge_base(
|
|
181
|
-
instance=instance, kb_data=kb_data
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
# Delete knowledge base if needed
|
|
185
|
-
if requires_delete:
|
|
186
|
-
logging.info("Knowledge base is corrupt. Reinstalling...")
|
|
187
|
-
delete_knowledge_base(instance=instance, kb_id=kb_id)
|
|
188
|
-
|
|
189
|
-
# Install the knowledge base
|
|
190
|
-
if requires_install:
|
|
191
|
-
create_knowledge_base(instance=instance, kb_data=kb_data)
|
|
192
|
-
|
|
193
|
-
# Confirm that the knowledge base was installed correctly
|
|
194
349
|
kb_id, requires_install, requires_delete = check_knowledge_base(
|
|
195
|
-
instance=instance,
|
|
350
|
+
instance=instance,
|
|
351
|
+
kb_name=kb_name,
|
|
352
|
+
kb_data=kb_data,
|
|
353
|
+
disable_commenting=disable_commenting,
|
|
196
354
|
)
|
|
197
|
-
assert not requires_install or requires_delete, "Knowledge base installation failed."
|
|
198
|
-
logging.info("Knowledge base installation succeeded.")
|
|
199
355
|
|
|
200
|
-
|
|
201
|
-
|
|
356
|
+
# Delete knowledge base if needed
|
|
357
|
+
if requires_delete:
|
|
358
|
+
logging.info(f"Knowledge base {kb_name} is corrupt. Reinstalling...")
|
|
359
|
+
delete_knowledge_base(instance=instance, kb_id=kb_id, kb_name=kb_name)
|
|
360
|
+
|
|
361
|
+
# Install the knowledge base
|
|
362
|
+
if requires_install:
|
|
363
|
+
create_knowledge_base(
|
|
364
|
+
instance=instance,
|
|
365
|
+
kb_name=kb_name,
|
|
366
|
+
kb_data=kb_data,
|
|
367
|
+
disable_commenting=disable_commenting,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Confirm that the knowledge base was installed correctly
|
|
371
|
+
kb_id, requires_install, requires_delete = check_knowledge_base(
|
|
372
|
+
instance=instance, kb_name=kb_name, kb_data=kb_data
|
|
373
|
+
)
|
|
374
|
+
assert (
|
|
375
|
+
not requires_install or requires_delete
|
|
376
|
+
), f"Knowledge base {kb_name} installation failed."
|
|
377
|
+
logging.info(f"Knowledge base {kb_name} installation succeeded.")
|
|
378
|
+
|
|
379
|
+
if not requires_delete and not requires_install:
|
|
380
|
+
logging.info(f"Knowledge base {kb_name} is already installed.")
|
|
202
381
|
|
|
203
382
|
|
|
204
383
|
def setup_workflows():
|
|
@@ -247,118 +426,119 @@ def install_workflows():
|
|
|
247
426
|
Notes: requires interacting with the UI, so we use playwright instead of the API
|
|
248
427
|
|
|
249
428
|
"""
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
page = browser.new_page()
|
|
254
|
-
ui_login(instance, page)
|
|
429
|
+
logging.info("Installing workflow update sets...")
|
|
430
|
+
for wf in WORKFLOWS.values():
|
|
431
|
+
_install_update_set(path=wf["update_set"], name=wf["name"])
|
|
255
432
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
433
|
+
|
|
434
|
+
def display_all_expected_columns(
|
|
435
|
+
instance: SNowInstance, list_name: str, expected_columns: list[str]
|
|
436
|
+
):
|
|
437
|
+
"""
|
|
438
|
+
Display all expected columns in a given list view.
|
|
439
|
+
|
|
440
|
+
Parameters:
|
|
441
|
+
-----------
|
|
442
|
+
instance: SNowInstance
|
|
443
|
+
The ServiceNow instance to configure.
|
|
444
|
+
list_name: str
|
|
445
|
+
The name of the list to display columns for.
|
|
446
|
+
expected_columns: list[str]
|
|
447
|
+
The list of columns to display.
|
|
448
|
+
|
|
449
|
+
"""
|
|
450
|
+
logging.info(f"... Setting up default view for list {list_name}")
|
|
451
|
+
|
|
452
|
+
# Get the default view (for all users)
|
|
453
|
+
logging.info(f"...... Fetching default view for list {list_name}...")
|
|
454
|
+
default_view = table_api_call(
|
|
455
|
+
instance=instance,
|
|
456
|
+
table="sys_ui_list",
|
|
457
|
+
params={
|
|
458
|
+
"sysparm_query": f"name={list_name}^view.title=Default View^sys_userISEMPTY^parentISEMPTY",
|
|
459
|
+
"sysparm_fields": "sys_id,name,view.title,sys_user",
|
|
460
|
+
},
|
|
461
|
+
)["result"]
|
|
462
|
+
|
|
463
|
+
# If there is more than one, delete all but the one with the most recently updated
|
|
464
|
+
if len(default_view) > 1:
|
|
465
|
+
logging.info(
|
|
466
|
+
f"......... Multiple default views found for list {list_name}. Deleting all but the most recent one."
|
|
467
|
+
)
|
|
468
|
+
default_view = sorted(default_view, key=lambda x: x["sys_updated_on"], reverse=True)
|
|
469
|
+
# Delete all but the first one
|
|
470
|
+
for view in default_view[1:]:
|
|
471
|
+
logging.info(f"............ Deleting view {view['sys_id']}")
|
|
472
|
+
table_api_call(
|
|
473
|
+
instance=instance, table=f"sys_ui_list/{view['sys_id']}", method="DELETE"
|
|
263
474
|
)
|
|
264
|
-
|
|
265
|
-
with page.expect_file_chooser() as fc_info:
|
|
266
|
-
iframe.locator("#attachFile").click()
|
|
267
|
-
file_chooser = fc_info.value
|
|
268
|
-
file_chooser.set_files(wf["update_set"])
|
|
269
|
-
iframe.locator("input:text('Upload')").click()
|
|
475
|
+
default_view = default_view[0]
|
|
270
476
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
477
|
+
# Find all columns in the view (get their sysid)
|
|
478
|
+
logging.info(f"...... Fetching existing columns for default view of list {list_name}...")
|
|
479
|
+
columns = table_api_call(
|
|
480
|
+
instance=instance,
|
|
481
|
+
table="sys_ui_list_element",
|
|
482
|
+
params={"sysparm_query": f"list_id={default_view['sys_id']}", "sysparm_fields": "sys_id"},
|
|
483
|
+
)["result"]
|
|
484
|
+
|
|
485
|
+
# Delete all columns in the default view
|
|
486
|
+
logging.info(f"...... Deleting existing columns for default view of list {list_name}...")
|
|
487
|
+
for column in columns:
|
|
488
|
+
table_api_call(
|
|
489
|
+
instance=instance, table=f"sys_ui_list_element/{column['sys_id']}", method="DELETE"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Add all expected columns to the default view
|
|
493
|
+
logging.info(f"...... Adding expected columns to default view of list {list_name}...")
|
|
494
|
+
for i, column in enumerate(expected_columns):
|
|
495
|
+
logging.info(f"......... {column}")
|
|
496
|
+
table_api_call(
|
|
275
497
|
instance=instance,
|
|
276
|
-
table="
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
+ "^state=loaded",
|
|
282
|
-
},
|
|
283
|
-
)["result"]
|
|
284
|
-
# ... apply them
|
|
285
|
-
for update_set in sys_remote_update_set:
|
|
286
|
-
logging.info(f"... {update_set['name']}")
|
|
287
|
-
page.goto(
|
|
288
|
-
instance.snow_url + "/sys_remote_update_set.do?sys_id=" + update_set["sys_id"]
|
|
289
|
-
)
|
|
290
|
-
page.locator("button:has-text('Preview Update Set')").first.click()
|
|
291
|
-
page.wait_for_selector("text=success")
|
|
292
|
-
# click escape to close popup
|
|
293
|
-
page.keyboard.press("Escape")
|
|
294
|
-
page.locator("button:has-text('Commit Update Set')").first.click()
|
|
295
|
-
page.wait_for_selector("text=Succeeded")
|
|
498
|
+
table="sys_ui_list_element",
|
|
499
|
+
method="POST",
|
|
500
|
+
data=json.dumps({"list_id": default_view["sys_id"], "element": column, "position": i}),
|
|
501
|
+
)
|
|
502
|
+
logging.info(f"...... Done.")
|
|
296
503
|
|
|
297
|
-
browser.close()
|
|
298
504
|
|
|
505
|
+
def check_all_columns_displayed(
|
|
506
|
+
instance: SNowInstance, url: str, expected_columns: list[str]
|
|
507
|
+
) -> bool:
|
|
508
|
+
"""
|
|
509
|
+
Get the visible columns and checks that all expected columns are displayed.
|
|
299
510
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
frame = page.wait_for_selector("iframe#gsft_main").content_frame()
|
|
315
|
-
# Open list personalization view
|
|
316
|
-
frame.click(
|
|
317
|
-
'i[data-title="Personalize List Columns"]'
|
|
318
|
-
) # CSS selector to support both unmodified and modified list views
|
|
319
|
-
selected_columns = frame.get_by_label("Selected", exact=True)
|
|
320
|
-
selected_columns_required = set()
|
|
321
|
-
# Required columns that are already added
|
|
322
|
-
for option in selected_columns.get_by_role("option").all():
|
|
323
|
-
value = option.get_attribute("value")
|
|
324
|
-
if value:
|
|
325
|
-
if value in expected_columns:
|
|
326
|
-
selected_columns_required.add(value)
|
|
327
|
-
# Remove extra columns
|
|
328
|
-
else:
|
|
329
|
-
option.click()
|
|
330
|
-
frame.get_by_text("Remove", exact=True).click()
|
|
331
|
-
columns_to_add = set(expected_columns) - selected_columns_required
|
|
332
|
-
|
|
333
|
-
# Add required columns
|
|
334
|
-
for column in columns_to_add:
|
|
335
|
-
# Using CSS selector because some elements can't be selected otherwise (e.g. "sys_class_name")
|
|
336
|
-
frame.click(f'option[value="{column}"]')
|
|
337
|
-
frame.get_by_text("Add").click()
|
|
338
|
-
frame.click("#ok_button")
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def check_all_columns_displayed(url: str, expected_columns: set[str]) -> bool:
|
|
342
|
-
"""Get the visible columns and checks that all expected columns are displayed."""
|
|
511
|
+
Parameters:
|
|
512
|
+
-----------
|
|
513
|
+
instance: SNowInstance
|
|
514
|
+
The ServiceNow instance to configure.
|
|
515
|
+
url: str
|
|
516
|
+
The URL of the list view to check.
|
|
517
|
+
expected_columns: list[str]
|
|
518
|
+
The set of columns to check for.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
--------
|
|
522
|
+
bool: True if all expected columns are displayed, False otherwise.
|
|
523
|
+
|
|
524
|
+
"""
|
|
343
525
|
with sync_playwright() as playwright:
|
|
344
|
-
instance = SNowInstance()
|
|
345
526
|
browser = playwright.chromium.launch(headless=True, slow_mo=1000)
|
|
346
527
|
page = browser.new_page()
|
|
347
|
-
|
|
528
|
+
url_login(instance, page)
|
|
348
529
|
page.goto(instance.snow_url + url)
|
|
349
|
-
iframe = page.
|
|
530
|
+
iframe = page.wait_for_selector("iframe#gsft_main").content_frame()
|
|
531
|
+
# Wait for gsft_main.GlideList2 to be available
|
|
532
|
+
page.wait_for_function("typeof gsft_main.GlideList2 !== 'undefined'")
|
|
350
533
|
lst = iframe.locator("table.data_list_table")
|
|
351
|
-
lst.wait_for()
|
|
352
534
|
|
|
353
535
|
# Validate the number of lists on the page
|
|
354
536
|
lst = lst.nth(0)
|
|
355
537
|
js_selector = f"gsft_main.GlideList2.get('{lst.get_attribute('data-list_id')}')"
|
|
356
|
-
# Wait for gsft_main.GlideList2 to be available
|
|
357
|
-
page.wait_for_function("typeof gsft_main.GlideList2 !== 'undefined'")
|
|
358
538
|
visible_columns = set(page.evaluate(f"{js_selector}.fields").split(","))
|
|
359
539
|
|
|
360
540
|
# check if expected columns is contained in the visible columns
|
|
361
|
-
if not expected_columns.issubset(visible_columns):
|
|
541
|
+
if not set(expected_columns).issubset(visible_columns):
|
|
362
542
|
logging.info(
|
|
363
543
|
f"Error setting up list at {url} \n Expected {expected_columns} columns, but got {visible_columns}."
|
|
364
544
|
)
|
|
@@ -368,42 +548,60 @@ def check_all_columns_displayed(url: str, expected_columns: set[str]) -> bool:
|
|
|
368
548
|
|
|
369
549
|
|
|
370
550
|
def setup_list_columns():
|
|
371
|
-
"""
|
|
551
|
+
"""
|
|
552
|
+
Setup the list view to display the expected number of columns.
|
|
553
|
+
|
|
554
|
+
"""
|
|
555
|
+
logging.info("Setting up visible list columns...")
|
|
372
556
|
list_mappings = {
|
|
373
557
|
"alm_asset": {
|
|
374
|
-
"url": "/now/nav/ui/classic/params/target/alm_asset_list.do
|
|
558
|
+
"url": "/now/nav/ui/classic/params/target/alm_asset_list.do",
|
|
375
559
|
"expected_columns_path": EXPECTED_ASSET_LIST_COLUMNS_PATH,
|
|
376
560
|
},
|
|
377
561
|
"alm_hardware": {
|
|
378
|
-
"url": "/now/nav/ui/classic/params/target/alm_hardware_list.do
|
|
562
|
+
"url": "/now/nav/ui/classic/params/target/alm_hardware_list.do",
|
|
379
563
|
"expected_columns_path": EXPECTED_HARDWARE_COLUMNS_PATH,
|
|
380
564
|
},
|
|
381
565
|
"change_request": {
|
|
382
|
-
"url": "/now/nav/ui/classic/params/target/change_request_list.do
|
|
566
|
+
"url": "/now/nav/ui/classic/params/target/change_request_list.do",
|
|
383
567
|
"expected_columns_path": EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
|
|
384
568
|
},
|
|
385
569
|
"incident": {
|
|
386
|
-
"url": "/now/nav/ui/classic/params/target/incident_list.do
|
|
570
|
+
"url": "/now/nav/ui/classic/params/target/incident_list.do",
|
|
387
571
|
"expected_columns_path": EXPECTED_INCIDENT_COLUMNS_PATH,
|
|
388
572
|
},
|
|
389
573
|
"sys_user": {
|
|
390
|
-
"url": "/now/nav/ui/classic/params/target/sys_user_list.do
|
|
574
|
+
"url": "/now/nav/ui/classic/params/target/sys_user_list.do",
|
|
391
575
|
"expected_columns_path": EXPECTED_USER_COLUMNS_PATH,
|
|
392
576
|
},
|
|
393
577
|
"sc_cat_item": {
|
|
394
|
-
"url": "/now/nav/ui/classic/params/target/sc_cat_item_list.do
|
|
578
|
+
"url": "/now/nav/ui/classic/params/target/sc_cat_item_list.do",
|
|
395
579
|
"expected_columns_path": EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
|
|
396
580
|
},
|
|
397
581
|
}
|
|
582
|
+
|
|
583
|
+
logging.info("... Creating a new user account to validate list columns")
|
|
584
|
+
admin_instance = SNowInstance()
|
|
585
|
+
username, password, usysid = create_user(instance=admin_instance)
|
|
586
|
+
user_instance = SNowInstance(snow_credentials=(username, password))
|
|
587
|
+
|
|
398
588
|
for task, task_info in list_mappings.items():
|
|
399
589
|
expected_columns_path = task_info["expected_columns_path"]
|
|
400
590
|
with open(expected_columns_path, "r") as f:
|
|
401
|
-
expected_columns =
|
|
402
|
-
|
|
591
|
+
expected_columns = list(json.load(f))
|
|
592
|
+
|
|
593
|
+
# Configuration is done via API (with admin credentials)
|
|
594
|
+
display_all_expected_columns(admin_instance, task, expected_columns=expected_columns)
|
|
595
|
+
|
|
596
|
+
# Validation is done via UI (with normal user credentials to see if changes have propagated)
|
|
403
597
|
assert check_all_columns_displayed(
|
|
404
|
-
task_info["url"], expected_columns=expected_columns
|
|
598
|
+
user_instance, task_info["url"], expected_columns=expected_columns
|
|
405
599
|
), f"Error setting up list columns at {task_info['url']}"
|
|
406
600
|
|
|
601
|
+
# Delete the user account
|
|
602
|
+
logging.info("... Deleting the test user account")
|
|
603
|
+
table_api_call(instance=admin_instance, table=f"sys_user/{usysid}", method="DELETE")
|
|
604
|
+
|
|
407
605
|
|
|
408
606
|
@retry(
|
|
409
607
|
stop=stop_after_attempt(3),
|
|
@@ -411,15 +609,15 @@ def setup_list_columns():
|
|
|
411
609
|
reraise=True,
|
|
412
610
|
before_sleep=lambda _: logging.info("Retrying due to a TimeoutError..."),
|
|
413
611
|
)
|
|
414
|
-
def process_form_fields(url: str, expected_fields: list[str], action: str):
|
|
612
|
+
def process_form_fields(instance: SNowInstance, url: str, expected_fields: list[str], action: str):
|
|
415
613
|
"""Process form fields based on the given action."""
|
|
416
614
|
with sync_playwright() as playwright:
|
|
417
|
-
instance = SNowInstance()
|
|
418
615
|
browser = playwright.chromium.launch(headless=True, slow_mo=1000)
|
|
419
616
|
page = browser.new_page()
|
|
420
|
-
|
|
617
|
+
url_login(instance, page)
|
|
421
618
|
page.goto(instance.snow_url + url)
|
|
422
619
|
frame = page.wait_for_selector("iframe#gsft_main").content_frame()
|
|
620
|
+
page.wait_for_function("typeof gsft_main.GlideList2 !== 'undefined'")
|
|
423
621
|
# Open form personalization view if not expanded
|
|
424
622
|
form_personalization_expanded = frame.locator(
|
|
425
623
|
'button:has-text("Personalize Form")'
|
|
@@ -427,7 +625,7 @@ def process_form_fields(url: str, expected_fields: list[str], action: str):
|
|
|
427
625
|
if form_personalization_expanded == "false":
|
|
428
626
|
frame.click('button:has-text("Personalize Form")')
|
|
429
627
|
available_options = (
|
|
430
|
-
frame.get_by_label("Personalize Form").locator('li[role="presentation"] input').all()
|
|
628
|
+
frame.get_by_label("Personalize Form").locator('li[role="presentation"] >> input').all()
|
|
431
629
|
)
|
|
432
630
|
|
|
433
631
|
for option in available_options:
|
|
@@ -438,9 +636,9 @@ def process_form_fields(url: str, expected_fields: list[str], action: str):
|
|
|
438
636
|
checked = option.get_attribute("aria-checked")
|
|
439
637
|
if action == "display":
|
|
440
638
|
if id in expected_fields and checked == "false":
|
|
441
|
-
option.click()
|
|
639
|
+
option.evaluate("e => e.click()") # playwright clicking doesn't work
|
|
442
640
|
elif id not in expected_fields and checked == "true":
|
|
443
|
-
option.click()
|
|
641
|
+
option.evaluate("e => e.click()") # playwright clicking doesn't work
|
|
444
642
|
elif action == "check":
|
|
445
643
|
if id in expected_fields and checked == "false":
|
|
446
644
|
logging.info(
|
|
@@ -454,6 +652,7 @@ def process_form_fields(url: str, expected_fields: list[str], action: str):
|
|
|
454
652
|
return False
|
|
455
653
|
if action == "check":
|
|
456
654
|
logging.info(f"All fields properly displayed for {url}.")
|
|
655
|
+
|
|
457
656
|
# Close the form personalization view
|
|
458
657
|
frame.click('button:has-text("Personalize Form")')
|
|
459
658
|
return True
|
|
@@ -482,19 +681,59 @@ def setup_form_fields():
|
|
|
482
681
|
"url": "/now/nav/ui/classic/params/target/sys_user.do",
|
|
483
682
|
},
|
|
484
683
|
}
|
|
684
|
+
|
|
685
|
+
logging.info("... Creating a new user account to validate form fields")
|
|
686
|
+
admin_instance = SNowInstance()
|
|
687
|
+
username, password, usysid = create_user(instance=admin_instance)
|
|
688
|
+
user_instance = SNowInstance(snow_credentials=(username, password))
|
|
689
|
+
|
|
485
690
|
for task, task_info in task_mapping.items():
|
|
486
691
|
expected_fields_path = task_info["expected_fields_path"]
|
|
487
692
|
with open(expected_fields_path, "r") as f:
|
|
488
693
|
expected_fields = json.load(f)
|
|
489
|
-
|
|
694
|
+
|
|
695
|
+
logging.info(f"Setting up form fields for {task}...")
|
|
696
|
+
process_form_fields(
|
|
697
|
+
admin_instance, task_info["url"], expected_fields=expected_fields, action="display"
|
|
698
|
+
)
|
|
699
|
+
sleep(5)
|
|
700
|
+
|
|
701
|
+
# If the view was edited, a new user preference was created for the admin user
|
|
702
|
+
# We want to apply it to all users so we need to edit the record to set sys_user to empty
|
|
703
|
+
# and system to true.
|
|
704
|
+
logging.info(f"Checking for new user preferences for {task} form fields")
|
|
705
|
+
user_preferences = table_api_call(
|
|
706
|
+
instance=admin_instance,
|
|
707
|
+
table="sys_user_preference",
|
|
708
|
+
params={
|
|
709
|
+
"sysparm_query": f"name=personalize_{task_info['url'].split('/')[-1].strip().replace('.do', '')}_default"
|
|
710
|
+
},
|
|
711
|
+
)["result"]
|
|
712
|
+
if len(user_preferences) > 0:
|
|
713
|
+
logging.info(f"Generalizing new settings to all users for {task} form fields")
|
|
714
|
+
# Get the most recent user preference
|
|
715
|
+
user_preference = sorted(
|
|
716
|
+
user_preferences, key=lambda x: x["sys_updated_on"], reverse=True
|
|
717
|
+
)[0]
|
|
718
|
+
# Update the user preference
|
|
719
|
+
table_api_call(
|
|
720
|
+
instance=admin_instance,
|
|
721
|
+
table=f"sys_user_preference/{user_preference['sys_id']}",
|
|
722
|
+
method="PATCH",
|
|
723
|
+
json={"user": "", "system": "true"},
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
# Validation is done with a new user to make sure the changes have propagated
|
|
727
|
+
logging.info(f"Validating form fields for {task}...")
|
|
490
728
|
assert process_form_fields(
|
|
491
|
-
task_info["url"], expected_fields=expected_fields, action="check"
|
|
729
|
+
user_instance, task_info["url"], expected_fields=expected_fields, action="check"
|
|
492
730
|
), f"Error setting up form fields at {task_info['url']}"
|
|
493
|
-
logging.info("All form fields properly displayed.")
|
|
494
|
-
print("all columns displayed")
|
|
495
731
|
|
|
732
|
+
# Delete the user account
|
|
733
|
+
logging.info("... Deleting the test user account")
|
|
734
|
+
table_api_call(instance=admin_instance, table=f"sys_user/{usysid}", method="DELETE")
|
|
496
735
|
|
|
497
|
-
|
|
736
|
+
logging.info("All form fields properly displayed.")
|
|
498
737
|
|
|
499
738
|
|
|
500
739
|
def check_instance_release_support():
|
|
@@ -515,8 +754,190 @@ def check_instance_release_support():
|
|
|
515
754
|
f"You are running {version_info['build name']} {version_info}."
|
|
516
755
|
)
|
|
517
756
|
return False
|
|
757
|
+
return True
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def enable_url_login():
|
|
761
|
+
"""
|
|
762
|
+
Configure the instance to allow login via URL.
|
|
763
|
+
|
|
764
|
+
"""
|
|
765
|
+
_set_sys_property(property_name="glide.security.restrict.get.login", value="false")
|
|
766
|
+
logging.info("URL login enabled.")
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def disable_guided_tours():
|
|
770
|
+
"""
|
|
771
|
+
Hide guided tour popups
|
|
772
|
+
|
|
773
|
+
"""
|
|
774
|
+
_set_sys_property(property_name="com.snc.guided_tours.sp.enable", value="false")
|
|
775
|
+
_set_sys_property(property_name="com.snc.guided_tours.standard_ui.enable", value="false")
|
|
776
|
+
logging.info("Guided tours disabled.")
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def disable_welcome_help_popup():
|
|
780
|
+
"""
|
|
781
|
+
Disable the welcome help popup
|
|
782
|
+
|
|
783
|
+
"""
|
|
784
|
+
set_user_preference(instance=SNowInstance(), key="overview_help.visited.navui", value="true")
|
|
785
|
+
logging.info("Welcome help popup disabled.")
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def disable_analytics_popups():
|
|
789
|
+
"""
|
|
790
|
+
Disable analytics popups (needs to be done through UI since Vancouver release)
|
|
791
|
+
|
|
792
|
+
"""
|
|
793
|
+
_set_sys_property(property_name="glide.analytics.enabled", value="false")
|
|
794
|
+
logging.info("Analytics popups disabled.")
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def setup_ui_themes():
|
|
798
|
+
"""
|
|
799
|
+
Install custom UI themes and set it as default
|
|
800
|
+
|
|
801
|
+
"""
|
|
802
|
+
logging.info("Installing custom UI themes...")
|
|
803
|
+
_install_update_set(path=UI_THEMES_UPDATE_SET["update_set"], name=UI_THEMES_UPDATE_SET["name"])
|
|
804
|
+
check_ui_themes_installed()
|
|
805
|
+
|
|
806
|
+
logging.info("Setting default UI theme")
|
|
807
|
+
_set_sys_property(
|
|
808
|
+
property_name="glide.ui.polaris.theme.custom",
|
|
809
|
+
value=get_workarena_theme_variants(SNowInstance())[0]["theme.sys_id"],
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
# Set admin user's theme variant
|
|
813
|
+
# ... get user's sysid
|
|
814
|
+
admin_user = table_api_call(
|
|
815
|
+
instance=SNowInstance(),
|
|
816
|
+
table="sys_user",
|
|
817
|
+
params={"sysparm_query": "user_name=admin", "sysparm_fields": "sys_id"},
|
|
818
|
+
)["result"][0]
|
|
819
|
+
# ... set user preference
|
|
820
|
+
set_user_preference(
|
|
821
|
+
instance=SNowInstance(),
|
|
822
|
+
user=admin_user["sys_id"],
|
|
823
|
+
key="glide.ui.polaris.theme.variant",
|
|
824
|
+
value=[
|
|
825
|
+
x["style.sys_id"]
|
|
826
|
+
for x in get_workarena_theme_variants(SNowInstance())
|
|
827
|
+
if x["style.name"] == "Workarena"
|
|
828
|
+
][0],
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def check_ui_themes_installed():
|
|
833
|
+
"""
|
|
834
|
+
Check if the UI themes are installed in the instance.
|
|
835
|
+
|
|
836
|
+
"""
|
|
837
|
+
expected_variants = set([v.lower() for v in UI_THEMES_UPDATE_SET["variants"]])
|
|
838
|
+
installed_themes = get_workarena_theme_variants(SNowInstance())
|
|
839
|
+
installed_themes = set([t["style.name"].lower() for t in installed_themes])
|
|
840
|
+
|
|
841
|
+
assert (
|
|
842
|
+
installed_themes == expected_variants
|
|
843
|
+
), f"""UI theme installation failed.
|
|
844
|
+
Expected: {expected_variants}
|
|
845
|
+
Installed: {installed_themes}
|
|
846
|
+
"""
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def set_home_page():
|
|
850
|
+
logging.info("Setting default home page")
|
|
851
|
+
_set_sys_property(property_name="glide.login.home", value="/now/nav/ui/home")
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def wipe_system_admin_preferences():
|
|
855
|
+
"""
|
|
856
|
+
Wipe all system admin preferences
|
|
857
|
+
|
|
858
|
+
"""
|
|
859
|
+
logging.info("Wiping all system admin preferences")
|
|
860
|
+
sys_admin_prefs = table_api_call(
|
|
861
|
+
instance=SNowInstance(),
|
|
862
|
+
table="sys_user_preference",
|
|
863
|
+
params={"sysparm_query": "user.user_name=admin", "sysparm_fields": "sys_id,name"},
|
|
864
|
+
)["result"]
|
|
865
|
+
|
|
866
|
+
# Delete all sysadmin preferences
|
|
867
|
+
logging.info("... Deleting all preferences")
|
|
868
|
+
for pref in sys_admin_prefs:
|
|
869
|
+
logging.info(f"...... deleting {pref['name']}")
|
|
870
|
+
table_api_call(
|
|
871
|
+
instance=SNowInstance(), table=f"sys_user_preference/{pref['sys_id']}", method="DELETE"
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def patch_report_filters():
|
|
876
|
+
"""
|
|
877
|
+
Add filters to reports to make sure they stay frozen in time and don't show new data
|
|
878
|
+
as then instance's life cycle progresses.
|
|
879
|
+
|
|
880
|
+
"""
|
|
881
|
+
logging.info("Patching reports with date filter...")
|
|
882
|
+
|
|
883
|
+
cutoff_date = REPORT_DATE_FILTER
|
|
884
|
+
|
|
885
|
+
instance = SNowInstance()
|
|
886
|
+
|
|
887
|
+
# Get all reports that are not already patched
|
|
888
|
+
reports = table_api_call(
|
|
889
|
+
instance=instance,
|
|
890
|
+
table="sys_report",
|
|
891
|
+
params={
|
|
892
|
+
"sysparm_query": f"sys_class_name=sys_report^active=true^descriptionNOT LIKE{REPORT_PATCH_FLAG}^ORdescriptionISEMPTY"
|
|
893
|
+
},
|
|
894
|
+
)["result"]
|
|
518
895
|
|
|
896
|
+
incompatible_reports = []
|
|
897
|
+
for report in reports:
|
|
898
|
+
# Find all sys_created_on columns of this record. Some have many.
|
|
899
|
+
sys_created_on_cols = [
|
|
900
|
+
c for c in table_column_info(instance, report["table"]).keys() if "sys_created_on" in c
|
|
901
|
+
]
|
|
902
|
+
|
|
903
|
+
try:
|
|
904
|
+
# XXX: We purposely do not support reports with multiple filter conditions for simplicity
|
|
905
|
+
if len(sys_created_on_cols) == 0 or "^NQ" in report["filter"]:
|
|
906
|
+
raise NotImplementedError()
|
|
907
|
+
|
|
908
|
+
# Add the filter
|
|
909
|
+
filter = "".join(
|
|
910
|
+
[
|
|
911
|
+
f"^{col}<javascript:gs.dateGenerate('{cutoff_date}','00:00:00')"
|
|
912
|
+
for col in sys_created_on_cols
|
|
913
|
+
]
|
|
914
|
+
) + ("^" if len(report["filter"]) > 0 and not report["filter"].startswith("^") else "")
|
|
915
|
+
table_api_call(
|
|
916
|
+
instance=instance,
|
|
917
|
+
table=f"sys_report/{report['sys_id']}",
|
|
918
|
+
method="PATCH",
|
|
919
|
+
json={
|
|
920
|
+
"filter": filter + report["filter"],
|
|
921
|
+
"description": report["description"] + " " + REPORT_PATCH_FLAG,
|
|
922
|
+
},
|
|
923
|
+
)
|
|
924
|
+
logging.info(
|
|
925
|
+
f"Patched report {report['title']} {report['sys_id']} (columns: {sys_created_on_cols})..."
|
|
926
|
+
)
|
|
519
927
|
|
|
928
|
+
except (NotImplementedError, HTTPError):
|
|
929
|
+
# HTTPError occurs when some reports simply cannot be patched because they are critical and protected
|
|
930
|
+
incompatible_reports.append(report["sys_id"])
|
|
931
|
+
logging.info(
|
|
932
|
+
f"Did not patch report {report['title']} {report['title']} (columns: {sys_created_on_cols})..."
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
@tenacity.retry(
|
|
937
|
+
stop=tenacity.stop_after_attempt(3),
|
|
938
|
+
reraise=True,
|
|
939
|
+
before_sleep=lambda _: logging.info("An error occurred. Retrying..."),
|
|
940
|
+
)
|
|
520
941
|
def setup():
|
|
521
942
|
"""
|
|
522
943
|
Check that WorkArena is installed correctly in the instance.
|
|
@@ -525,12 +946,42 @@ def setup():
|
|
|
525
946
|
if not check_instance_release_support():
|
|
526
947
|
return # Don't continue if the instance is not supported
|
|
527
948
|
|
|
949
|
+
# Enable URL login (XXX: Do this first since other functions can use URL login)
|
|
950
|
+
enable_url_login()
|
|
951
|
+
|
|
952
|
+
# Set default landing page
|
|
953
|
+
set_home_page()
|
|
954
|
+
|
|
955
|
+
# Disable popups for new users
|
|
956
|
+
# ... guided tours
|
|
957
|
+
disable_guided_tours()
|
|
958
|
+
# ... analytics
|
|
959
|
+
disable_analytics_popups()
|
|
960
|
+
# ... help
|
|
961
|
+
disable_welcome_help_popup()
|
|
962
|
+
|
|
963
|
+
# Install custom UI themes (needs to be after disabling popups)
|
|
964
|
+
setup_ui_themes()
|
|
965
|
+
|
|
966
|
+
# Clear all predefined system admin preferences (e.g., default list views, etc.)
|
|
967
|
+
wipe_system_admin_preferences()
|
|
968
|
+
|
|
969
|
+
# Patch all reports to only show data <= April 1, 2024
|
|
970
|
+
patch_report_filters()
|
|
971
|
+
|
|
528
972
|
# XXX: Install workflows first because they may automate some downstream installations
|
|
529
973
|
setup_workflows()
|
|
530
|
-
|
|
974
|
+
setup_knowledge_bases()
|
|
975
|
+
|
|
531
976
|
# Setup the user list columns by displaying all columns and checking that the expected number are displayed
|
|
532
|
-
setup_list_columns()
|
|
533
977
|
setup_form_fields()
|
|
978
|
+
setup_list_columns()
|
|
979
|
+
|
|
980
|
+
# Save installation date
|
|
981
|
+
logging.info("Saving installation date")
|
|
982
|
+
_set_sys_property(property_name="workarena.installation.date", value=datetime.now().isoformat())
|
|
983
|
+
|
|
984
|
+
logging.info("WorkArena setup complete.")
|
|
534
985
|
|
|
535
986
|
|
|
536
987
|
def main():
|
|
@@ -539,4 +990,25 @@ def main():
|
|
|
539
990
|
|
|
540
991
|
"""
|
|
541
992
|
logging.basicConfig(level=logging.INFO)
|
|
993
|
+
|
|
994
|
+
try:
|
|
995
|
+
past_install_date = _get_sys_property("workarena.installation.date")
|
|
996
|
+
logging.info(f"Detected previous installation on {past_install_date}. Reinstalling...")
|
|
997
|
+
except:
|
|
998
|
+
past_install_date = "never"
|
|
999
|
+
|
|
1000
|
+
logging.info(
|
|
1001
|
+
f"""
|
|
1002
|
+
|
|
1003
|
+
██ ██ ██████ ██████ ██ ██ █████ ██████ ███████ ███ ██ █████
|
|
1004
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██
|
|
1005
|
+
██ █ ██ ██ ██ ██████ █████ ███████ ██████ █████ ██ ██ ██ ███████
|
|
1006
|
+
██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
1007
|
+
███ ███ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ██ ████ ██ ██
|
|
1008
|
+
|
|
1009
|
+
Instance: {SNowInstance().snow_url}
|
|
1010
|
+
Previous installation: {past_install_date}
|
|
1011
|
+
|
|
1012
|
+
"""
|
|
1013
|
+
)
|
|
542
1014
|
setup()
|