Skip to content

Commit f48fbef

Browse files
authored
Merge pull request #90 from ServiceNow/fix-issue-86
Fix issue with date filters for reports
2 parents 638fcf2 + 6444a53 commit f48fbef

File tree

8 files changed

+212
-64
lines changed

8 files changed

+212
-64
lines changed

src/browsergym/workarena/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.4.3"
1+
__version__ = "0.4.4"
22

33
import inspect
44
from logging import warning
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from .utils import table_api_call
2+
3+
4+
def set_sys_property(instance, property_name: str, value: str):
5+
"""
6+
Set a sys_property in the instance.
7+
8+
Parameters:
9+
-----------
10+
instance: SNowInstance
11+
The instance to set the property in
12+
property_name: str
13+
The name of the property to set
14+
value: str
15+
The value to set for the property
16+
17+
"""
18+
19+
property = table_api_call(
20+
instance=instance,
21+
table="sys_properties",
22+
params={"sysparm_query": f"name={property_name}", "sysparm_fields": "sys_id"},
23+
)["result"]
24+
25+
if not property:
26+
property_sysid = ""
27+
method = "POST"
28+
else:
29+
property_sysid = "/" + property[0]["sys_id"]
30+
method = "PUT"
31+
32+
property = table_api_call(
33+
instance=instance,
34+
table=f"sys_properties{property_sysid}",
35+
method=method,
36+
json={"name": property_name, "value": value},
37+
)
38+
39+
# Verify that the property was updated
40+
assert property["result"]["value"] == value, f"Error setting {property_name}."
41+
42+
43+
def get_sys_property(instance, property_name: str) -> str:
44+
"""
45+
Get a sys_property from the instance.
46+
47+
Parameters:
48+
-----------
49+
instance: SNowInstance
50+
The instance to get the property from
51+
property_name: str
52+
The name of the property to get
53+
54+
Returns:
55+
--------
56+
str
57+
The value of the property
58+
59+
"""
60+
property_value = table_api_call(
61+
instance=instance,
62+
table="sys_properties",
63+
params={"sysparm_query": f"name={property_name}", "sysparm_fields": "value"},
64+
)["result"][0]["value"]
65+
66+
return property_value

src/browsergym/workarena/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from importlib import resources
2+
from json import load as json_load
3+
from os.path import exists
24

35
from ..workarena import data_files
46
from ..workarena.tasks import utils
@@ -224,4 +226,4 @@
224226

225227
# Report date filter patch flag
226228
REPORT_PATCH_FLAG = "WORKARENA_DATE_FILTER_PATCH"
227-
REPORT_DATE_FILTER = "2025-07-15"
229+
REPORT_FILTER_PROPERTY = "workarena.report.filter.config"

src/browsergym/workarena/data_files/task_configs/report_retrieval_minmax_task.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/browsergym/workarena/data_files/task_configs/report_retrieval_value_task.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/browsergym/workarena/install.py

Lines changed: 91 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from requests import HTTPError
1111
from time import sleep
1212

13+
from .api.system_properties import get_sys_property, set_sys_property
1314
from .api.ui_themes import get_workarena_theme_variants
1415
from .api.user import create_user
1516
from .api.utils import table_api_call, table_column_info
@@ -38,7 +39,7 @@
3839
EXPECTED_USER_FORM_FIELDS_PATH,
3940
# Patch flag for reports
4041
REPORT_PATCH_FLAG,
41-
REPORT_DATE_FILTER,
42+
REPORT_FILTER_PROPERTY,
4243
# Supported ServiceNow releases
4344
SNOW_SUPPORTED_RELEASES,
4445
# For workflows setup
@@ -51,51 +52,22 @@
5152
from .utils import url_login
5253

5354

54-
def _set_sys_property(property_name: str, value: str):
55+
def _is_dev_portal_instance() -> bool:
5556
"""
56-
Set a sys_property in the instance.
57+
Check if the instance is a ServiceNow Developer Portal instance.
5758
58-
"""
59-
instance = SNowInstance()
60-
61-
property = table_api_call(
62-
instance=instance,
63-
table="sys_properties",
64-
params={"sysparm_query": f"name={property_name}", "sysparm_fields": "sys_id"},
65-
)["result"]
66-
67-
if not property:
68-
property_sysid = ""
69-
method = "POST"
70-
else:
71-
property_sysid = "/" + property[0]["sys_id"]
72-
method = "PUT"
73-
74-
property = table_api_call(
75-
instance=instance,
76-
table=f"sys_properties{property_sysid}",
77-
method=method,
78-
json={"name": property_name, "value": value},
79-
)
80-
81-
# Verify that the property was updated
82-
assert property["result"]["value"] == value, f"Error setting {property_name}."
83-
84-
85-
def _get_sys_property(property_name: str) -> str:
86-
"""
87-
Get a sys_property from the instance.
59+
Returns:
60+
--------
61+
bool: True if the instance is a developer portal instance, False otherwise.
8862
8963
"""
9064
instance = SNowInstance()
91-
92-
property_value = table_api_call(
93-
instance=instance,
94-
table="sys_properties",
95-
params={"sysparm_query": f"name={property_name}", "sysparm_fields": "value"},
96-
)["result"][0]["value"]
97-
98-
return property_value
65+
# Check if the instance url has the for devXXXXXX.service-now.com format (where X is a digit)
66+
if re.match(r"^https?://dev\d{6}\.service-now\.com", instance.snow_url):
67+
logging.info("Detected a developer portal instance...")
68+
return True
69+
logging.info("Detected an internal instance...")
70+
return False
9971

10072

10173
def _install_update_set(path: str, name: str):
@@ -797,7 +769,9 @@ def enable_url_login():
797769
Configure the instance to allow login via URL.
798770
799771
"""
800-
_set_sys_property(property_name="glide.security.restrict.get.login", value="false")
772+
set_sys_property(
773+
instance=SNowInstance(), property_name="glide.security.restrict.get.login", value="false"
774+
)
801775
logging.info("URL login enabled.")
802776

803777

@@ -808,7 +782,21 @@ def disable_password_policies():
808782
Notes: this is required to allow the creation of users with weak passwords.
809783
810784
"""
811-
_set_sys_property(property_name="glide.security.password.policy.enabled", value="false")
785+
set_sys_property(
786+
instance=SNowInstance(),
787+
property_name="glide.security.password.policy.enabled",
788+
value="false",
789+
)
790+
set_sys_property(
791+
instance=SNowInstance(), property_name="glide.apply.password_policy.on_login", value="false"
792+
)
793+
# The following is not supported on developer portal instances
794+
if not _is_dev_portal_instance():
795+
set_sys_property(
796+
instance=SNowInstance(),
797+
property_name="glide.authenticate.api.user.reset_password.mandatory",
798+
value="false",
799+
)
812800
logging.info("Password policies disabled.")
813801

814802

@@ -817,8 +805,14 @@ def disable_guided_tours():
817805
Hide guided tour popups
818806
819807
"""
820-
_set_sys_property(property_name="com.snc.guided_tours.sp.enable", value="false")
821-
_set_sys_property(property_name="com.snc.guided_tours.standard_ui.enable", value="false")
808+
set_sys_property(
809+
instance=SNowInstance(), property_name="com.snc.guided_tours.sp.enable", value="false"
810+
)
811+
set_sys_property(
812+
instance=SNowInstance(),
813+
property_name="com.snc.guided_tours.standard_ui.enable",
814+
value="false",
815+
)
822816
logging.info("Guided tours disabled.")
823817

824818

@@ -836,7 +830,9 @@ def disable_analytics_popups():
836830
Disable analytics popups (needs to be done through UI since Vancouver release)
837831
838832
"""
839-
_set_sys_property(property_name="glide.analytics.enabled", value="false")
833+
set_sys_property(
834+
instance=SNowInstance(), property_name="glide.analytics.enabled", value="false"
835+
)
840836
logging.info("Analytics popups disabled.")
841837

842838

@@ -850,7 +846,8 @@ def setup_ui_themes():
850846
check_ui_themes_installed()
851847

852848
logging.info("Setting default UI theme")
853-
_set_sys_property(
849+
set_sys_property(
850+
instance=SNowInstance(),
854851
property_name="glide.ui.polaris.theme.custom",
855852
value=get_workarena_theme_variants(SNowInstance())[0]["theme.sys_id"],
856853
)
@@ -894,7 +891,9 @@ def check_ui_themes_installed():
894891

895892
def set_home_page():
896893
logging.info("Setting default home page")
897-
_set_sys_property(property_name="glide.login.home", value="/now/nav/ui/home")
894+
set_sys_property(
895+
instance=SNowInstance(), property_name="glide.login.home", value="/now/nav/ui/home"
896+
)
898897

899898

900899
def wipe_system_admin_preferences():
@@ -918,9 +917,9 @@ def wipe_system_admin_preferences():
918917
)
919918

920919

921-
def is_report_filter_using_time(filter):
920+
def is_report_filter_using_relative_time(filter):
922921
"""
923-
Heuristic to check if a report is filtering based on time
922+
Heuristic to check if a report is filtering based on relative time
924923
925924
This aims to detect the use of functions like "gs.endOfToday()". To avoid hardcoding all of them,
926925
we simply check for the use of keywords. Our filter is definitely too wide, but that's ok.
@@ -938,6 +937,32 @@ def patch_report_filters():
938937
logging.info("Patching reports with date filter...")
939938

940939
instance = SNowInstance()
940+
filter_config = instance.report_filter_config
941+
942+
# If the report date filter is already set, we use the existing values (would be the case on reinstall)
943+
if not filter_config:
944+
# Set the report date filter to current date as YYYY-MM-DD and time filter to current time as HH:MM:SS
945+
now = datetime.now()
946+
report_date_filter = now.strftime("%Y-%m-%d")
947+
report_time_filter = now.strftime("%H:%M:%S")
948+
# ... save the filter config
949+
logging.info(
950+
f"Setting report date filter to {report_date_filter} and time filter to {report_time_filter} via {REPORT_FILTER_PROPERTY}"
951+
)
952+
set_sys_property(
953+
instance=instance,
954+
property_name=REPORT_FILTER_PROPERTY,
955+
value=json.dumps(
956+
{"report_date_filter": report_date_filter, "report_time_filter": report_time_filter}
957+
),
958+
)
959+
else:
960+
# Use the existing configuration
961+
logging.info(
962+
f"Using existing report date filter {filter_config['report_date_filter']} and time filter {filter_config['report_time_filter']}"
963+
)
964+
report_date_filter = filter_config["report_date_filter"]
965+
report_time_filter = filter_config["report_time_filter"]
941966

942967
# Get all reports that are not already patched
943968
reports = table_api_call(
@@ -959,27 +984,31 @@ def patch_report_filters():
959984
logging.info(f"Discarding report {report['title']} {report['sys_id']}...")
960985
raise NotImplementedError() # Mark for deletion
961986

962-
if not is_report_filter_using_time(report["filter"]):
987+
if not is_report_filter_using_relative_time(report["filter"]):
963988
# That's a report we want to keep (use date cutoff filter)
964-
filter_date = REPORT_DATE_FILTER
989+
filter_date = report_date_filter
990+
filter_time = report_time_filter
965991
logging.info(
966992
f"Keeping report {report['title']} {report['sys_id']} (columns: {sys_created_on_cols})..."
967993
)
968994
else:
969-
# XXX: We do not support reports with filters that rely on time (e.g., last 10 days) because
995+
# XXX: We do not support reports with filters that rely on relative time (e.g., last 10 days) because
970996
# there are not stable. In this case, we don't delete them but add a filter to make
971997
# them empty. They will be shown as "No data available".
972998
logging.info(
973999
f"Disabling report {report['title']} {report['sys_id']} because it uses time filters..."
9741000
)
9751001
filter_date = "1900-01-01"
1002+
filter_time = "00:00:00"
9761003

1004+
# Format the filter
9771005
filter = "".join(
9781006
[
979-
f"^{col}<javascript:gs.dateGenerate('{filter_date}','00:00:00')"
1007+
f"^{col}<javascript:gs.dateGenerate('{filter_date}','{filter_time}')"
9801008
for col in sys_created_on_cols
9811009
]
9821010
) + ("^" if len(report["filter"]) > 0 and not report["filter"].startswith("^") else "")
1011+
# Patch the report with the new filter
9831012
table_api_call(
9841013
instance=instance,
9851014
table=f"sys_report/{report['sys_id']}",
@@ -1055,7 +1084,11 @@ def setup():
10551084

10561085
# Save installation date
10571086
logging.info("Saving installation date")
1058-
_set_sys_property(property_name="workarena.installation.date", value=datetime.now().isoformat())
1087+
set_sys_property(
1088+
instance=SNowInstance(),
1089+
property_name="workarena.installation.date",
1090+
value=datetime.now().isoformat(),
1091+
)
10591092

10601093
logging.info("WorkArena setup complete.")
10611094

@@ -1068,7 +1101,9 @@ def main():
10681101
logging.basicConfig(level=logging.INFO)
10691102

10701103
try:
1071-
past_install_date = _get_sys_property("workarena.installation.date")
1104+
past_install_date = get_sys_property(
1105+
instance=SNowInstance(), property_name="workarena.installation.date"
1106+
)
10721107
logging.info(f"Detected previous installation on {past_install_date}. Reinstalling...")
10731108
except:
10741109
past_install_date = "never"

src/browsergym/workarena/instance.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import json
12
import os
23
import requests
34
import re
45

56
from playwright.sync_api import sync_playwright
67
from typing import Optional
78

8-
from .config import SNOW_BROWSER_TIMEOUT
9+
from .config import SNOW_BROWSER_TIMEOUT, REPORT_FILTER_PROPERTY
910

1011

1112
class SNowInstance:
@@ -124,3 +125,25 @@ def release_version(self) -> str:
124125
browser.close()
125126

126127
return release_info
128+
129+
@property
130+
def report_filter_config(self) -> dict:
131+
"""
132+
Get the report filter configuration from the ServiceNow instance.
133+
134+
Returns:
135+
--------
136+
dict
137+
The report filter configuration, or an empty dictionary if not found.
138+
139+
"""
140+
from .api.system_properties import (
141+
get_sys_property,
142+
) # Import here to avoid circular import issues
143+
144+
try:
145+
config = get_sys_property(self, REPORT_FILTER_PROPERTY)
146+
config = json.loads(config)
147+
return config
148+
except Exception:
149+
return None

0 commit comments

Comments
 (0)