mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-11 18:37:33 -04:00
Fixed disable button, formated time to include time zone (#2850)
This commit is contained in:
+6
-2
@@ -1726,8 +1726,12 @@ class TemplateFolderForm(StripWhitespaceForm):
|
|||||||
def __init__(self, all_service_users=None, *args, **kwargs):
|
def __init__(self, all_service_users=None, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if all_service_users is not None:
|
if all_service_users is not None:
|
||||||
regular_users = [user for user in all_service_users if not user.platform_admin]
|
regular_users = [
|
||||||
platform_admins = [user for user in all_service_users if user.platform_admin]
|
user for user in all_service_users if not user.platform_admin
|
||||||
|
]
|
||||||
|
platform_admins = [
|
||||||
|
user for user in all_service_users if user.platform_admin
|
||||||
|
]
|
||||||
|
|
||||||
self.users_with_permission.all_service_users = regular_users
|
self.users_with_permission.all_service_users = regular_users
|
||||||
self.users_with_permission.choices = [
|
self.users_with_permission.choices = [
|
||||||
|
|||||||
@@ -34,14 +34,16 @@ def get_report_info(service_id, report_name, s3_config):
|
|||||||
# check_s3_file_exists already called obj.load(), so metadata should be populated
|
# check_s3_file_exists already called obj.load(), so metadata should be populated
|
||||||
size_bytes = obj.content_length
|
size_bytes = obj.content_length
|
||||||
|
|
||||||
if size_bytes < 1024:
|
# Only show as available if file has any content (not empty)
|
||||||
size_str = f"{size_bytes} B"
|
if size_bytes > 0:
|
||||||
elif size_bytes < 1024 * 1024:
|
if size_bytes < 1024:
|
||||||
size_str = f"{size_bytes / 1024:.1f} KB"
|
size_str = f"{size_bytes} B"
|
||||||
else:
|
elif size_bytes < 1024 * 1024:
|
||||||
size_str = f"{size_bytes / (1024 * 1024):.1f} MB"
|
size_str = f"{size_bytes / 1024:.1f} KB"
|
||||||
|
else:
|
||||||
|
size_str = f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||||
|
|
||||||
return {"available": True, "size": size_str}
|
return {"available": True, "size": size_str}
|
||||||
except Exception: # nosec B110
|
except Exception: # nosec B110
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,11 @@ def download_notifications_csv(service_id):
|
|||||||
)
|
)
|
||||||
s3_file_content = s3download(service_id, s3_report_id)
|
s3_file_content = s3download(service_id, s3_report_id)
|
||||||
return Response(
|
return Response(
|
||||||
stream_with_context(convert_s3_csv_timestamps(s3_file_content)),
|
stream_with_context(
|
||||||
|
convert_s3_csv_timestamps(
|
||||||
|
s3_file_content, user_timezone=user_tz_name
|
||||||
|
)
|
||||||
|
),
|
||||||
mimetype="text/csv",
|
mimetype="text/csv",
|
||||||
headers={
|
headers={
|
||||||
"Content-Disposition": 'inline; filename="{} - {} - {} report.csv"'.format(
|
"Content-Disposition": 'inline; filename="{} - {} - {} report.csv"'.format(
|
||||||
@@ -185,7 +189,6 @@ def download_notifications_csv(service_id):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
except S3ObjectNotFound:
|
except S3ObjectNotFound:
|
||||||
# Edge case: File was deleted between page load and download attempt
|
|
||||||
current_app.logger.warning(
|
current_app.logger.warning(
|
||||||
f"File {s3_report_id} was expected but not found for service {service_id}. "
|
f"File {s3_report_id} was expected but not found for service {service_id}. "
|
||||||
"It may have been deleted after page load."
|
"It may have been deleted after page load."
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ def process_folder_management_form(form, current_folder_id):
|
|||||||
current_service.id,
|
current_service.id,
|
||||||
name=form.get_folder_name(),
|
name=form.get_folder_name(),
|
||||||
parent_id=current_folder_id,
|
parent_id=current_folder_id,
|
||||||
created_by_id=str(current_user.id)
|
created_by_id=str(current_user.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
if form.is_move_op:
|
if form.is_move_op:
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ from app.notify_client import NotifyAdminAPIClient, cache
|
|||||||
|
|
||||||
class TemplateFolderAPIClient(NotifyAdminAPIClient):
|
class TemplateFolderAPIClient(NotifyAdminAPIClient):
|
||||||
@cache.delete("service-{service_id}-template-folders")
|
@cache.delete("service-{service_id}-template-folders")
|
||||||
def create_template_folder(self, service_id, name, parent_id=None, created_by_id=None):
|
def create_template_folder(
|
||||||
|
self, service_id, name, parent_id=None, created_by_id=None
|
||||||
|
):
|
||||||
data = {"name": name, "parent_id": parent_id}
|
data = {"name": name, "parent_id": parent_id}
|
||||||
if created_by_id:
|
if created_by_id:
|
||||||
data["created_by_id"] = created_by_id
|
data["created_by_id"] = created_by_id
|
||||||
|
|||||||
@@ -203,7 +203,7 @@
|
|||||||
<button class="usa-button width-full"
|
<button class="usa-button width-full"
|
||||||
disabled
|
disabled
|
||||||
aria-label="Yesterday's report not available - no messages were sent">
|
aria-label="Yesterday's report not available - no messages were sent">
|
||||||
Yesterday - No messages sent
|
Yesterday - No messages sent
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+48
-18
@@ -179,36 +179,66 @@ def generate_notifications_csv(**kwargs):
|
|||||||
raise Exception("Should never reach here")
|
raise Exception("Should never reach here")
|
||||||
|
|
||||||
|
|
||||||
def convert_report_date_to_preferred_timezone(db_date_str_in_utc):
|
def convert_report_date_to_preferred_timezone(db_date_str_in_utc, target_timezone=None):
|
||||||
"""
|
"""
|
||||||
Report dates in the db are in UTC. We need to convert them to the user's default timezone,
|
Report dates in the db are in UTC. We need to convert them to the user's default timezone,
|
||||||
which defaults to "US/Eastern"
|
which defaults to "US/Eastern"
|
||||||
|
Format: "2025-05-14 07:22:18 AM America/Los_Angeles"
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_date_str_in_utc: UTC datetime string in format "YYYY-MM-DD HH:MM:SS"
|
||||||
|
target_timezone: Optional timezone name to use (e.g. "US/Eastern", "America/Los_Angeles")
|
||||||
|
If not provided, will use current user's preference or default
|
||||||
"""
|
"""
|
||||||
date_arr = db_date_str_in_utc.split(" ")
|
try:
|
||||||
db_date_str_in_utc = f"{date_arr[0]}T{date_arr[1]}+00:00"
|
date_arr = db_date_str_in_utc.split(" ")
|
||||||
utc_date_obj = datetime.datetime.fromisoformat(db_date_str_in_utc)
|
db_date_str_in_utc = f"{date_arr[0]}T{date_arr[1]}+00:00"
|
||||||
|
utc_date_obj = datetime.datetime.fromisoformat(db_date_str_in_utc)
|
||||||
|
|
||||||
utc_date_obj = utc_date_obj.replace(tzinfo=ZoneInfo("UTC"))
|
utc_date_obj = utc_date_obj.replace(tzinfo=ZoneInfo("UTC"))
|
||||||
preferred_timezone = get_user_preferred_timezone_obj()
|
|
||||||
preferred_date_obj = utc_date_obj.astimezone(preferred_timezone)
|
|
||||||
preferred_tz_created_at = preferred_date_obj.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
return preferred_tz_created_at
|
if not target_timezone:
|
||||||
|
target_timezone = get_user_preferred_timezone()
|
||||||
|
|
||||||
|
preferred_timezone = ZoneInfo(target_timezone)
|
||||||
|
preferred_date_obj = utc_date_obj.astimezone(preferred_timezone)
|
||||||
|
|
||||||
|
formatted_date = preferred_date_obj.strftime("%Y-%m-%d %I:%M:%S %p")
|
||||||
|
preferred_tz_created_at = f"{formatted_date} {target_timezone}"
|
||||||
|
return preferred_tz_created_at
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
try:
|
||||||
|
current_app.logger.error(
|
||||||
|
f"Error converting timezone for {db_date_str_in_utc}: {e}"
|
||||||
|
)
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
return f"{db_date_str_in_utc} UTC"
|
||||||
|
|
||||||
|
|
||||||
_timezone_cache = {}
|
_timezone_cache = {}
|
||||||
|
|
||||||
|
|
||||||
def get_user_preferred_timezone():
|
def get_user_preferred_timezone():
|
||||||
if current_user and hasattr(current_user, "preferred_timezone"):
|
try:
|
||||||
tz = current_user.preferred_timezone
|
if (
|
||||||
# Validate timezone using ZoneInfo - it will raise if invalid
|
current_user
|
||||||
try:
|
and current_user.is_authenticated
|
||||||
ZoneInfo(tz)
|
and hasattr(current_user, "preferred_timezone")
|
||||||
return tz
|
):
|
||||||
except ZoneInfoNotFoundError:
|
tz = current_user.preferred_timezone
|
||||||
# Invalid timezone, fall back to default
|
if tz:
|
||||||
pass
|
try:
|
||||||
|
ZoneInfo(tz)
|
||||||
|
return tz
|
||||||
|
except ZoneInfoNotFoundError:
|
||||||
|
pass
|
||||||
|
except (AttributeError, RuntimeError):
|
||||||
|
# AttributeError: current_user might not have expected attributes
|
||||||
|
# RuntimeError: working outside of request context
|
||||||
|
pass
|
||||||
|
|
||||||
return "US/Eastern"
|
return "US/Eastern"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+16
-3
@@ -1,13 +1,26 @@
|
|||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
|
|
||||||
from app.utils.csv import convert_report_date_to_preferred_timezone
|
from app.utils.csv import (
|
||||||
|
convert_report_date_to_preferred_timezone,
|
||||||
|
get_user_preferred_timezone,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def convert_s3_csv_timestamps(csv_content):
|
def convert_s3_csv_timestamps(csv_content, user_timezone=None):
|
||||||
|
"""
|
||||||
|
Convert UTC timestamps in CSV to user's preferred timezone.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
csv_content: The CSV content as string or bytes
|
||||||
|
user_timezone: Optional pre-captured timezone (to avoid context issues in streaming)
|
||||||
|
"""
|
||||||
if isinstance(csv_content, bytes):
|
if isinstance(csv_content, bytes):
|
||||||
csv_content = csv_content.decode("utf-8")
|
csv_content = csv_content.decode("utf-8")
|
||||||
|
|
||||||
|
if user_timezone is None:
|
||||||
|
user_timezone = get_user_preferred_timezone()
|
||||||
|
|
||||||
reader = csv.reader(io.StringIO(csv_content))
|
reader = csv.reader(io.StringIO(csv_content))
|
||||||
|
|
||||||
time_column_index = None
|
time_column_index = None
|
||||||
@@ -39,7 +52,7 @@ def convert_s3_csv_timestamps(csv_content):
|
|||||||
if len(row) > time_column_index and row[time_column_index]:
|
if len(row) > time_column_index and row[time_column_index]:
|
||||||
try:
|
try:
|
||||||
row[time_column_index] = convert_report_date_to_preferred_timezone(
|
row[time_column_index] = convert_report_date_to_preferred_timezone(
|
||||||
row[time_column_index]
|
row[time_column_index], target_timezone=user_timezone
|
||||||
)
|
)
|
||||||
except Exception: # nosec B110
|
except Exception: # nosec B110
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -107,5 +107,9 @@ def test_s3_csv_gets_timezone_converted(
|
|||||||
_test_page_title=False,
|
_test_page_title=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_convert.assert_called_once_with(b"csv,data")
|
# Now it passes the user_timezone parameter
|
||||||
|
assert mock_convert.call_count == 1
|
||||||
|
call_args = mock_convert.call_args
|
||||||
|
assert call_args[0][0] == b"csv,data"
|
||||||
|
assert "user_timezone" in call_args[1]
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|||||||
@@ -1500,7 +1500,10 @@ def test_new_folder_is_created_if_only_new_folder_is_filled_out(
|
|||||||
|
|
||||||
assert mock_move_to_template_folder.called is False
|
assert mock_move_to_template_folder.called is False
|
||||||
mock_create_template_folder.assert_called_once_with(
|
mock_create_template_folder.assert_called_once_with(
|
||||||
SERVICE_ONE_ID, name="new folder", parent_id=None, created_by_id="6ce466d0-fd6a-11e5-82f5-e0accb9d11a6"
|
SERVICE_ONE_ID,
|
||||||
|
name="new folder",
|
||||||
|
parent_id=None,
|
||||||
|
created_by_id="6ce466d0-fd6a-11e5-82f5-e0accb9d11a6",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1538,7 +1541,10 @@ def test_should_be_able_to_move_to_new_folder(
|
|||||||
)
|
)
|
||||||
|
|
||||||
mock_create_template_folder.assert_called_once_with(
|
mock_create_template_folder.assert_called_once_with(
|
||||||
SERVICE_ONE_ID, name="new folder", parent_id=None, created_by_id="6ce466d0-fd6a-11e5-82f5-e0accb9d11a6"
|
SERVICE_ONE_ID,
|
||||||
|
name="new folder",
|
||||||
|
parent_id=None,
|
||||||
|
created_by_id="6ce466d0-fd6a-11e5-82f5-e0accb9d11a6",
|
||||||
)
|
)
|
||||||
mock_move_to_template_folder.assert_called_once_with(
|
mock_move_to_template_folder.assert_called_once_with(
|
||||||
service_id=SERVICE_ONE_ID,
|
service_id=SERVICE_ONE_ID,
|
||||||
|
|||||||
@@ -90,14 +90,14 @@ def get_notifications_csv_mock(
|
|||||||
None,
|
None,
|
||||||
[
|
[
|
||||||
"Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time,Carrier\n",
|
"Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time,Carrier\n",
|
||||||
"8005555555,foo,,,Did not like it,Delivered,1943-04-19 08:00:00,AT&T Mobility\r\n",
|
"8005555555,foo,,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern,AT&T Mobility\r\n",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"Anne Example",
|
"Anne Example",
|
||||||
[
|
[
|
||||||
"Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time,Carrier\n",
|
"Phone Number,Template,Sent by,Batch File,Carrier Response,Status,Time,Carrier\n",
|
||||||
"8005555555,foo,Anne Example,,Did not like it,Delivered,1943-04-19 08:00:00,AT&T Mobility\r\n", # noqa
|
"8005555555,foo,Anne Example,,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern,AT&T Mobility\r\n", # noqa
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -145,7 +145,7 @@ def test_generate_notifications_csv_without_job(
|
|||||||
"bar.csv",
|
"bar.csv",
|
||||||
"Did not like it",
|
"Did not like it",
|
||||||
"Delivered",
|
"Delivered",
|
||||||
"1943-04-19 08:00:00",
|
"1943-04-19 08:00:00 AM US/Eastern",
|
||||||
"AT&T Mobility",
|
"AT&T Mobility",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -174,7 +174,7 @@ def test_generate_notifications_csv_without_job(
|
|||||||
"bar.csv",
|
"bar.csv",
|
||||||
"Did not like it",
|
"Did not like it",
|
||||||
"Delivered",
|
"Delivered",
|
||||||
"1943-04-19 08:00:00",
|
"1943-04-19 08:00:00 AM US/Eastern",
|
||||||
"AT&T Mobility",
|
"AT&T Mobility",
|
||||||
"🐜",
|
"🐜",
|
||||||
"🐝",
|
"🐝",
|
||||||
@@ -206,7 +206,7 @@ def test_generate_notifications_csv_without_job(
|
|||||||
"bar.csv",
|
"bar.csv",
|
||||||
"Did not like it",
|
"Did not like it",
|
||||||
"Delivered",
|
"Delivered",
|
||||||
"1943-04-19 08:00:00",
|
"1943-04-19 08:00:00 AM US/Eastern",
|
||||||
"AT&T Mobility",
|
"AT&T Mobility",
|
||||||
"🐜,🐜",
|
"🐜,🐜",
|
||||||
"🐝,🐝",
|
"🐝,🐝",
|
||||||
@@ -383,6 +383,47 @@ def test_get_errors_for_csv(
|
|||||||
|
|
||||||
|
|
||||||
def test_convert_report_date_to_preferred_timezone():
|
def test_convert_report_date_to_preferred_timezone():
|
||||||
|
"""Test that timezone conversion includes AM/PM and timezone name."""
|
||||||
original = "2023-11-16 05:00:00"
|
original = "2023-11-16 05:00:00"
|
||||||
altered = convert_report_date_to_preferred_timezone(original)
|
altered = convert_report_date_to_preferred_timezone(original)
|
||||||
assert altered == "2023-11-16 00:00:00"
|
assert altered == "2023-11-16 12:00:00 AM US/Eastern"
|
||||||
|
|
||||||
|
original = "2023-11-16 17:30:00"
|
||||||
|
altered = convert_report_date_to_preferred_timezone(original)
|
||||||
|
assert altered == "2023-11-16 12:30:00 PM US/Eastern"
|
||||||
|
|
||||||
|
original = "2023-11-16 17:00:00"
|
||||||
|
altered = convert_report_date_to_preferred_timezone(original)
|
||||||
|
assert altered == "2023-11-16 12:00:00 PM US/Eastern"
|
||||||
|
|
||||||
|
original = "2023-11-16 05:00:00"
|
||||||
|
altered = convert_report_date_to_preferred_timezone(original)
|
||||||
|
assert altered == "2023-11-16 12:00:00 AM US/Eastern"
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_report_date_with_custom_timezone(mocker):
|
||||||
|
"""Test timezone conversion with a user who has a custom timezone."""
|
||||||
|
mocker.patch(
|
||||||
|
"app.utils.csv.get_user_preferred_timezone", return_value="America/Los_Angeles"
|
||||||
|
)
|
||||||
|
|
||||||
|
original = "2023-11-16 15:22:18"
|
||||||
|
altered = convert_report_date_to_preferred_timezone(original)
|
||||||
|
assert altered == "2023-11-16 07:22:18 AM America/Los_Angeles"
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_report_date_with_explicit_timezone():
|
||||||
|
"""Test timezone conversion with explicitly provided timezone."""
|
||||||
|
original = "2023-11-16 15:22:18"
|
||||||
|
altered = convert_report_date_to_preferred_timezone(
|
||||||
|
original, target_timezone="America/Los_Angeles"
|
||||||
|
)
|
||||||
|
assert altered == "2023-11-16 07:22:18 AM America/Los_Angeles"
|
||||||
|
|
||||||
|
altered = convert_report_date_to_preferred_timezone(
|
||||||
|
original, target_timezone="US/Eastern"
|
||||||
|
)
|
||||||
|
assert altered == "2023-11-16 10:22:18 AM US/Eastern"
|
||||||
|
|
||||||
|
altered = convert_report_date_to_preferred_timezone(original, target_timezone="UTC")
|
||||||
|
assert altered == "2023-11-16 03:22:18 PM UTC"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def test_convert_s3_csv_timestamps_with_real_format():
|
|||||||
"app.utils.s3_csv.convert_report_date_to_preferred_timezone"
|
"app.utils.s3_csv.convert_report_date_to_preferred_timezone"
|
||||||
) as mock_convert:
|
) as mock_convert:
|
||||||
|
|
||||||
def mock_conversion(timestamp):
|
def mock_conversion(timestamp, target_timezone=None):
|
||||||
# Just return the timestamp as-is for testing
|
# Just return the timestamp as-is for testing
|
||||||
return timestamp
|
return timestamp
|
||||||
|
|
||||||
@@ -30,8 +30,12 @@ def test_convert_s3_csv_timestamps_with_real_format():
|
|||||||
in result[0]
|
in result[0]
|
||||||
)
|
)
|
||||||
assert mock_convert.call_count == 2
|
assert mock_convert.call_count == 2
|
||||||
mock_convert.assert_any_call("2024-03-15 17:19:00")
|
mock_convert.assert_any_call(
|
||||||
mock_convert.assert_any_call("2024-03-15 20:30:00")
|
"2024-03-15 17:19:00", target_timezone="US/Eastern"
|
||||||
|
)
|
||||||
|
mock_convert.assert_any_call(
|
||||||
|
"2024-03-15 20:30:00", target_timezone="US/Eastern"
|
||||||
|
)
|
||||||
assert "2024-03-15 17:19:00" in full_result
|
assert "2024-03-15 17:19:00" in full_result
|
||||||
assert "2024-03-15 20:30:00" in full_result
|
assert "2024-03-15 20:30:00" in full_result
|
||||||
|
|
||||||
@@ -92,7 +96,7 @@ Another Template,+12025555678,2024-01-15 21:45:00,delivered"""
|
|||||||
with patch(
|
with patch(
|
||||||
"app.utils.s3_csv.convert_report_date_to_preferred_timezone"
|
"app.utils.s3_csv.convert_report_date_to_preferred_timezone"
|
||||||
) as mock_convert:
|
) as mock_convert:
|
||||||
mock_convert.side_effect = lambda x: f"{x} Converted"
|
mock_convert.side_effect = lambda x, target_timezone=None: f"{x} Converted"
|
||||||
|
|
||||||
result = list(convert_s3_csv_timestamps(csv_content))
|
result = list(convert_s3_csv_timestamps(csv_content))
|
||||||
full_result = "".join(result)
|
full_result = "".join(result)
|
||||||
@@ -106,11 +110,11 @@ def test_actual_timezone_conversion():
|
|||||||
from app.utils.csv import convert_report_date_to_preferred_timezone
|
from app.utils.csv import convert_report_date_to_preferred_timezone
|
||||||
|
|
||||||
with patch("app.utils.csv.current_user") as mock_user:
|
with patch("app.utils.csv.current_user") as mock_user:
|
||||||
|
mock_user.is_authenticated = True
|
||||||
mock_user.preferred_timezone = "US/Eastern"
|
mock_user.preferred_timezone = "US/Eastern"
|
||||||
|
|
||||||
result = convert_report_date_to_preferred_timezone("2024-01-15 20:30:00")
|
result = convert_report_date_to_preferred_timezone("2024-01-15 20:30:00")
|
||||||
|
|
||||||
# Should be in 24-hour format without AM/PM or timezone
|
# Should now be in 12-hour format with AM/PM and timezone
|
||||||
assert "15:30:00" in result
|
assert "03:30:00 PM" in result
|
||||||
assert "PM" not in result
|
assert "US/Eastern" in result
|
||||||
assert "US/Eastern" not in result
|
|
||||||
|
|||||||
Reference in New Issue
Block a user