browsergym-workarena 0.1.0rc7__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 +3 -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/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/install.py +620 -155
- browsergym/workarena/tasks/base.py +85 -26
- browsergym/workarena/tasks/dashboard.py +620 -0
- browsergym/workarena/tasks/form.py +121 -85
- browsergym/workarena/tasks/knowledge.py +30 -14
- browsergym/workarena/tasks/list.py +121 -67
- 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 +2 -2
- 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.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/METADATA +7 -3
- {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/RECORD +32 -21
- {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/WHEEL +1 -1
- {browsergym_workarena-0.1.0rc7.dist-info → browsergym_workarena-0.2.0.dist-info}/entry_points.txt +0 -0
- {browsergym_workarena-0.1.0rc7.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,110 +426,106 @@ 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
|
-
columns_to_remove = []
|
|
321
|
-
selected_columns_required = set()
|
|
322
|
-
|
|
323
|
-
# Check for required columns that are already added, and extra columns that must be removed
|
|
324
|
-
|
|
325
|
-
for option in selected_columns.get_by_role("option").all():
|
|
326
|
-
value = option.get_attribute("value")
|
|
327
|
-
if value:
|
|
328
|
-
if value in expected_columns:
|
|
329
|
-
selected_columns_required.add(value)
|
|
330
|
-
else:
|
|
331
|
-
columns_to_remove.append(value)
|
|
332
|
-
|
|
333
|
-
# Remove extra columns
|
|
334
|
-
selected_columns.select_option(value=columns_to_remove)
|
|
335
|
-
frame.get_by_text("Remove", exact=True).click()
|
|
336
|
-
|
|
337
|
-
# Add missing columns
|
|
338
|
-
available_columns = frame.get_by_label("Available", exact=True)
|
|
339
|
-
columns_to_add = list(set(expected_columns) - selected_columns_required)
|
|
340
|
-
available_columns.select_option(value=columns_to_add)
|
|
341
|
-
frame.get_by_text("Add", exact=True).click()
|
|
342
|
-
|
|
343
|
-
# Confirm
|
|
344
|
-
frame.locator("#ok_button").click()
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
def check_all_columns_displayed(url: str, expected_columns: set[str]) -> bool:
|
|
348
|
-
"""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
|
+
"""
|
|
349
525
|
with sync_playwright() as playwright:
|
|
350
|
-
instance = SNowInstance()
|
|
351
526
|
browser = playwright.chromium.launch(headless=True, slow_mo=1000)
|
|
352
527
|
page = browser.new_page()
|
|
353
|
-
|
|
528
|
+
url_login(instance, page)
|
|
354
529
|
page.goto(instance.snow_url + url)
|
|
355
530
|
iframe = page.wait_for_selector("iframe#gsft_main").content_frame()
|
|
356
531
|
# Wait for gsft_main.GlideList2 to be available
|
|
@@ -363,7 +538,7 @@ def check_all_columns_displayed(url: str, expected_columns: set[str]) -> bool:
|
|
|
363
538
|
visible_columns = set(page.evaluate(f"{js_selector}.fields").split(","))
|
|
364
539
|
|
|
365
540
|
# check if expected columns is contained in the visible columns
|
|
366
|
-
if not expected_columns.issubset(visible_columns):
|
|
541
|
+
if not set(expected_columns).issubset(visible_columns):
|
|
367
542
|
logging.info(
|
|
368
543
|
f"Error setting up list at {url} \n Expected {expected_columns} columns, but got {visible_columns}."
|
|
369
544
|
)
|
|
@@ -373,42 +548,60 @@ def check_all_columns_displayed(url: str, expected_columns: set[str]) -> bool:
|
|
|
373
548
|
|
|
374
549
|
|
|
375
550
|
def setup_list_columns():
|
|
376
|
-
"""
|
|
551
|
+
"""
|
|
552
|
+
Setup the list view to display the expected number of columns.
|
|
553
|
+
|
|
554
|
+
"""
|
|
555
|
+
logging.info("Setting up visible list columns...")
|
|
377
556
|
list_mappings = {
|
|
378
557
|
"alm_asset": {
|
|
379
|
-
"url": "/now/nav/ui/classic/params/target/alm_asset_list.do
|
|
558
|
+
"url": "/now/nav/ui/classic/params/target/alm_asset_list.do",
|
|
380
559
|
"expected_columns_path": EXPECTED_ASSET_LIST_COLUMNS_PATH,
|
|
381
560
|
},
|
|
382
561
|
"alm_hardware": {
|
|
383
|
-
"url": "/now/nav/ui/classic/params/target/alm_hardware_list.do
|
|
562
|
+
"url": "/now/nav/ui/classic/params/target/alm_hardware_list.do",
|
|
384
563
|
"expected_columns_path": EXPECTED_HARDWARE_COLUMNS_PATH,
|
|
385
564
|
},
|
|
386
565
|
"change_request": {
|
|
387
|
-
"url": "/now/nav/ui/classic/params/target/change_request_list.do
|
|
566
|
+
"url": "/now/nav/ui/classic/params/target/change_request_list.do",
|
|
388
567
|
"expected_columns_path": EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
|
|
389
568
|
},
|
|
390
569
|
"incident": {
|
|
391
|
-
"url": "/now/nav/ui/classic/params/target/incident_list.do
|
|
570
|
+
"url": "/now/nav/ui/classic/params/target/incident_list.do",
|
|
392
571
|
"expected_columns_path": EXPECTED_INCIDENT_COLUMNS_PATH,
|
|
393
572
|
},
|
|
394
573
|
"sys_user": {
|
|
395
|
-
"url": "/now/nav/ui/classic/params/target/sys_user_list.do
|
|
574
|
+
"url": "/now/nav/ui/classic/params/target/sys_user_list.do",
|
|
396
575
|
"expected_columns_path": EXPECTED_USER_COLUMNS_PATH,
|
|
397
576
|
},
|
|
398
577
|
"sc_cat_item": {
|
|
399
|
-
"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",
|
|
400
579
|
"expected_columns_path": EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
|
|
401
580
|
},
|
|
402
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
|
+
|
|
403
588
|
for task, task_info in list_mappings.items():
|
|
404
589
|
expected_columns_path = task_info["expected_columns_path"]
|
|
405
590
|
with open(expected_columns_path, "r") as f:
|
|
406
|
-
expected_columns =
|
|
407
|
-
|
|
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)
|
|
408
597
|
assert check_all_columns_displayed(
|
|
409
|
-
task_info["url"], expected_columns=expected_columns
|
|
598
|
+
user_instance, task_info["url"], expected_columns=expected_columns
|
|
410
599
|
), f"Error setting up list columns at {task_info['url']}"
|
|
411
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
|
+
|
|
412
605
|
|
|
413
606
|
@retry(
|
|
414
607
|
stop=stop_after_attempt(3),
|
|
@@ -416,13 +609,12 @@ def setup_list_columns():
|
|
|
416
609
|
reraise=True,
|
|
417
610
|
before_sleep=lambda _: logging.info("Retrying due to a TimeoutError..."),
|
|
418
611
|
)
|
|
419
|
-
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):
|
|
420
613
|
"""Process form fields based on the given action."""
|
|
421
614
|
with sync_playwright() as playwright:
|
|
422
|
-
instance = SNowInstance()
|
|
423
615
|
browser = playwright.chromium.launch(headless=True, slow_mo=1000)
|
|
424
616
|
page = browser.new_page()
|
|
425
|
-
|
|
617
|
+
url_login(instance, page)
|
|
426
618
|
page.goto(instance.snow_url + url)
|
|
427
619
|
frame = page.wait_for_selector("iframe#gsft_main").content_frame()
|
|
428
620
|
page.wait_for_function("typeof gsft_main.GlideList2 !== 'undefined'")
|
|
@@ -433,7 +625,7 @@ def process_form_fields(url: str, expected_fields: list[str], action: str):
|
|
|
433
625
|
if form_personalization_expanded == "false":
|
|
434
626
|
frame.click('button:has-text("Personalize Form")')
|
|
435
627
|
available_options = (
|
|
436
|
-
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()
|
|
437
629
|
)
|
|
438
630
|
|
|
439
631
|
for option in available_options:
|
|
@@ -444,9 +636,9 @@ def process_form_fields(url: str, expected_fields: list[str], action: str):
|
|
|
444
636
|
checked = option.get_attribute("aria-checked")
|
|
445
637
|
if action == "display":
|
|
446
638
|
if id in expected_fields and checked == "false":
|
|
447
|
-
option.click()
|
|
639
|
+
option.evaluate("e => e.click()") # playwright clicking doesn't work
|
|
448
640
|
elif id not in expected_fields and checked == "true":
|
|
449
|
-
option.click()
|
|
641
|
+
option.evaluate("e => e.click()") # playwright clicking doesn't work
|
|
450
642
|
elif action == "check":
|
|
451
643
|
if id in expected_fields and checked == "false":
|
|
452
644
|
logging.info(
|
|
@@ -460,6 +652,7 @@ def process_form_fields(url: str, expected_fields: list[str], action: str):
|
|
|
460
652
|
return False
|
|
461
653
|
if action == "check":
|
|
462
654
|
logging.info(f"All fields properly displayed for {url}.")
|
|
655
|
+
|
|
463
656
|
# Close the form personalization view
|
|
464
657
|
frame.click('button:has-text("Personalize Form")')
|
|
465
658
|
return True
|
|
@@ -488,19 +681,59 @@ def setup_form_fields():
|
|
|
488
681
|
"url": "/now/nav/ui/classic/params/target/sys_user.do",
|
|
489
682
|
},
|
|
490
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
|
+
|
|
491
690
|
for task, task_info in task_mapping.items():
|
|
492
691
|
expected_fields_path = task_info["expected_fields_path"]
|
|
493
692
|
with open(expected_fields_path, "r") as f:
|
|
494
693
|
expected_fields = json.load(f)
|
|
495
|
-
|
|
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}...")
|
|
496
728
|
assert process_form_fields(
|
|
497
|
-
task_info["url"], expected_fields=expected_fields, action="check"
|
|
729
|
+
user_instance, task_info["url"], expected_fields=expected_fields, action="check"
|
|
498
730
|
), f"Error setting up form fields at {task_info['url']}"
|
|
499
|
-
logging.info("All form fields properly displayed.")
|
|
500
|
-
print("all columns displayed")
|
|
501
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")
|
|
502
735
|
|
|
503
|
-
|
|
736
|
+
logging.info("All form fields properly displayed.")
|
|
504
737
|
|
|
505
738
|
|
|
506
739
|
def check_instance_release_support():
|
|
@@ -524,6 +757,187 @@ def check_instance_release_support():
|
|
|
524
757
|
return True
|
|
525
758
|
|
|
526
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"]
|
|
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
|
+
)
|
|
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
|
+
)
|
|
527
941
|
def setup():
|
|
528
942
|
"""
|
|
529
943
|
Check that WorkArena is installed correctly in the instance.
|
|
@@ -532,12 +946,42 @@ def setup():
|
|
|
532
946
|
if not check_instance_release_support():
|
|
533
947
|
return # Don't continue if the instance is not supported
|
|
534
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
|
+
|
|
535
972
|
# XXX: Install workflows first because they may automate some downstream installations
|
|
536
973
|
setup_workflows()
|
|
537
|
-
|
|
974
|
+
setup_knowledge_bases()
|
|
975
|
+
|
|
538
976
|
# Setup the user list columns by displaying all columns and checking that the expected number are displayed
|
|
539
|
-
setup_list_columns()
|
|
540
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.")
|
|
541
985
|
|
|
542
986
|
|
|
543
987
|
def main():
|
|
@@ -546,4 +990,25 @@ def main():
|
|
|
546
990
|
|
|
547
991
|
"""
|
|
548
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
|
+
)
|
|
549
1014
|
setup()
|