From e828f7885355e4f73fb032c764a9d978c9c8417f Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 28 Jul 2025 17:04:53 -0700 Subject: [PATCH 01/10] fixing pagination and download links along with tests --- app/main/views/activity.py | 32 +++++++++++++- app/main/views/jobs.py | 7 ++- .../views/activity/all-activity.html | 41 ++++++++++++------ tests/app/main/views/test_activity.py | 43 +++++++++++++++++-- 4 files changed, 104 insertions(+), 19 deletions(-) diff --git a/app/main/views/activity.py b/app/main/views/activity.py index c4cd2d583..9dd60b35b 100644 --- a/app/main/views/activity.py +++ b/app/main/views/activity.py @@ -13,6 +13,32 @@ from app.utils.pagination import ( from app.utils.user import user_has_permissions +def get_download_availability(service_id): + """ + Check if there are jobs available for each download time period. + """ + jobs_1_day = job_api_client.get_page_of_jobs(service_id, page=1, limit_days=1) + jobs_3_days = job_api_client.get_page_of_jobs(service_id, page=1, limit_days=3) + jobs_5_days = job_api_client.get_page_of_jobs(service_id, page=1, limit_days=5) + jobs_7_days = job_api_client.get_immediate_jobs(service_id) + + has_1_day_data = len(generate_job_dict(jobs_1_day)) > 0 + has_3_day_data = len(generate_job_dict(jobs_3_days)) > 0 + has_5_day_data = len(generate_job_dict(jobs_5_days)) > 0 + has_7_day_data = len(jobs_7_days) > 0 + + return { + "has_1_day_data": has_1_day_data, + "has_3_day_data": has_3_day_data, + "has_5_day_data": has_5_day_data, + "has_7_day_data": has_7_day_data, + "has_any_download_data": has_1_day_data + or has_3_day_data + or has_5_day_data + or has_7_day_data, + } + + @main.route("/activity/services/") @user_has_permissions(ServicePermission.VIEW_ACTIVITY) def all_jobs_activity(service_id): @@ -22,6 +48,7 @@ def all_jobs_activity(service_id): all_jobs_dict = generate_job_dict(jobs) prev_page, next_page, pagination = handle_pagination(jobs, service_id, page) message_type = ("sms",) + download_availability = get_download_availability(service_id) return render_template( "views/activity/all-activity.html", all_jobs_dict=all_jobs_dict, @@ -29,6 +56,7 @@ def all_jobs_activity(service_id): next_page=next_page, prev_page=prev_page, pagination=pagination, + **download_availability, download_link_one_day=url_for( ".download_notifications_csv", service_id=current_service.id, @@ -68,9 +96,11 @@ def handle_pagination(jobs, service_id, page): if page > 1 else None ) + total_items = jobs.get("total", 0) + has_next_link = jobs.get("links", {}).get("next") is not None next_page = ( generate_next_dict("main.all_jobs_activity", service_id, page) - if jobs.get("links", {}).get("next") + if has_next_link and total_items > 50 else None ) pagination = generate_pagination_pages( diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index 596743af7..938a0849d 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -253,7 +253,12 @@ def get_notifications(service_id, message_type, status_override=None): # noqa ) next_page = None - if "links" in notifications and notifications["links"].get("next", None): + total_items = notifications.get("total", 0) + if ( + "links" in notifications + and notifications["links"].get("next", None) + and total_items > 50 + ): next_page = generate_next_dict( "main.view_notifications", service_id, page, url_args ) diff --git a/app/templates/views/activity/all-activity.html b/app/templates/views/activity/all-activity.html index d722414b3..94209a38c 100644 --- a/app/templates/views/activity/all-activity.html +++ b/app/templates/views/activity/all-activity.html @@ -126,20 +126,33 @@ {{show_pagination}} {% if current_user.has_permissions(ServicePermission.VIEW_ACTIVITY) %} -

Download recent reports

-

- Download all data last 24 hours (CSV) -

-

- Download all data last 3 days (CSV) -   -

-

- Download all data last 5 days (CSV) -

-

- Download all data last 7 days (CSV) -

+ {% if has_any_download_data %} +

Download recent reports

+ {% if has_1_day_data %} +

+ Download all data last 24 hours (CSV) +

+ {% endif %} + {% if has_3_day_data %} +

+ Download all data last 3 days (CSV) +   +

+ {% endif %} + {% if has_5_day_data %} +

+ Download all data last 5 days (CSV) +

+ {% endif %} + {% if has_7_day_data %} +

+ Download all data last 7 days (CSV) +

+ {% endif %} + {% else %} +

Download recent reports

+

No recent activity to download. Download links will appear when jobs are available.

+ {% endif %} {% endif %} {% endblock %} diff --git a/tests/app/main/views/test_activity.py b/tests/app/main/views/test_activity.py index 753931ce5..09e2ab4fd 100644 --- a/tests/app/main/views/test_activity.py +++ b/tests/app/main/views/test_activity.py @@ -9,6 +9,7 @@ from freezegun import freeze_time from app.main.views.jobs import get_status_filters, get_time_left from app.models.service import Service +from tests import notification_json from tests.conftest import ( SERVICE_ONE_ID, create_active_caseworking_user, @@ -465,12 +466,17 @@ def test_should_show_notifications_for_a_service_with_next_previous( client_request, service_one, active_user_with_permissions, - mock_get_notifications_with_previous_next, mock_get_service_statistics, mock_get_service_data_retention, mock_get_no_api_keys, mocker, ): + mocker.patch( + "app.notification_api_client.get_notifications_for_service", + return_value=notification_json( + service_one["id"], rows=50, with_links=True + ) | {"total": 100}, + ) page = client_request.get( "main.view_notifications", service_id=service_one["id"], @@ -504,16 +510,47 @@ def test_should_show_notifications_for_a_service_with_next_previous( assert "page 1" in prev_page_link.text.strip() -def test_doesnt_show_pagination_with_search_term( +def test_doesnt_show_pagination_when_50_or_fewer_items( client_request, service_one, active_user_with_permissions, - mock_get_notifications_with_previous_next, mock_get_service_statistics, mock_get_service_data_retention, mock_get_no_api_keys, mocker, ): + mocker.patch( + "app.notification_api_client.get_notifications_for_service", + return_value=notification_json( + service_one["id"], rows=50, with_links=False + ), + ) + page = client_request.get( + "main.view_notifications", + service_id=service_one["id"], + message_type="sms", + ) + + assert not page.find("a", {"rel": "next"}) + assert not page.find("a", {"rel": "previous"}) + assert not page.select_one(".table-show-more-link") + + +def test_doesnt_show_pagination_with_search_term( + client_request, + service_one, + active_user_with_permissions, + mock_get_service_statistics, + mock_get_service_data_retention, + mock_get_no_api_keys, + mocker, +): + mocker.patch( + "app.notification_api_client.get_notifications_for_service", + return_value=notification_json( + service_one["id"], rows=50, with_links=True + ) | {"total": 100}, + ) page = client_request.post( "main.view_notifications", service_id=service_one["id"], From ba787f8cd166a9abf858a2310fcf96d4d74219ae Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 28 Jul 2025 17:46:00 -0700 Subject: [PATCH 02/10] add test --- tests/app/main/views/test_activity.py | 88 +++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/app/main/views/test_activity.py b/tests/app/main/views/test_activity.py index 09e2ab4fd..f2431bf17 100644 --- a/tests/app/main/views/test_activity.py +++ b/tests/app/main/views/test_activity.py @@ -285,6 +285,94 @@ def test_link_to_download_notifications( ) +def test_download_links_show_when_data_available( + client_request, + service_one, + active_user_with_permissions, + mocker, +): + + mock_jobs_with_data = { + "data": [{"id": "job1", "created_at": "2020-01-01T00:00:00.000000+00:00"}], + "total": 1, + "page_size": 50 + } + mock_jobs_empty = {"data": [], "total": 0, "page_size": 50} + + mocker.patch("app.job_api_client.get_page_of_jobs", return_value=mock_jobs_with_data) + mocker.patch("app.job_api_client.get_immediate_jobs", return_value=[{"id": "job1"}]) + + page = client_request.get( + "main.all_jobs_activity", + service_id=service_one["id"], + ) + + assert "Download recent reports" in page.text + assert "Download all data last 24 hours" in page.text + assert "Download all data last 3 days" in page.text + assert "Download all data last 5 days" in page.text + assert "Download all data last 7 days" in page.text + assert "No recent activity to download" not in page.text + + +def test_download_links_partial_data_available( + client_request, + service_one, + active_user_with_permissions, + mocker, +): + mock_jobs_with_data = { + "data": [{"id": "job1", "created_at": "2020-01-01T00:00:00.000000+00:00"}], + "total": 1, + "page_size": 50 + } + mock_jobs_empty = {"data": [], "total": 0, "page_size": 50} + + def mock_get_page_of_jobs(service_id, page=1, limit_days=None): + if limit_days in [1, 5]: + return mock_jobs_with_data + return mock_jobs_empty + + mocker.patch("app.job_api_client.get_page_of_jobs", side_effect=mock_get_page_of_jobs) + mocker.patch("app.job_api_client.get_immediate_jobs", return_value=[]) + + page = client_request.get( + "main.all_jobs_activity", + service_id=service_one["id"], + ) + + assert "Download recent reports" in page.text + assert "Download all data last 24 hours" in page.text + assert "Download all data last 3 days" not in page.text + assert "Download all data last 5 days" in page.text + assert "Download all data last 7 days" not in page.text + assert "No recent activity to download" not in page.text + + +def test_download_links_no_data_available( + client_request, + service_one, + active_user_with_permissions, + mocker, +): + mock_jobs_empty = {"data": [], "total": 0, "page_size": 50} + + mocker.patch("app.job_api_client.get_page_of_jobs", return_value=mock_jobs_empty) + mocker.patch("app.job_api_client.get_immediate_jobs", return_value=[]) + + page = client_request.get( + "main.all_jobs_activity", + service_id=service_one["id"], + ) + + assert "Download recent reports" in page.text + assert "Download all data last 24 hours" not in page.text + assert "Download all data last 3 days" not in page.text + assert "Download all data last 5 days" not in page.text + assert "Download all data last 7 days" not in page.text + assert "No recent activity to download. Download links will appear when jobs are available." in page.text + + def test_download_not_available_to_users_without_dashboard( client_request, active_caseworking_user, From eac2733a33122c7f426b399635f3b322c9d56041 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Tue, 29 Jul 2025 09:57:59 -0700 Subject: [PATCH 03/10] flake8 --- tests/app/main/views/test_activity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/app/main/views/test_activity.py b/tests/app/main/views/test_activity.py index f2431bf17..9fd5118db 100644 --- a/tests/app/main/views/test_activity.py +++ b/tests/app/main/views/test_activity.py @@ -297,7 +297,6 @@ def test_download_links_show_when_data_available( "total": 1, "page_size": 50 } - mock_jobs_empty = {"data": [], "total": 0, "page_size": 50} mocker.patch("app.job_api_client.get_page_of_jobs", return_value=mock_jobs_with_data) mocker.patch("app.job_api_client.get_immediate_jobs", return_value=[{"id": "job1"}]) From d65be41da5804a5e4ea76ee87e0788b64104e71d Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Tue, 29 Jul 2025 10:17:35 -0700 Subject: [PATCH 04/10] fix test get_immediate_jobs for download availability check --- tests/app/main/views/test_jobs_activity.py | 26 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/tests/app/main/views/test_jobs_activity.py b/tests/app/main/views/test_jobs_activity.py index 125b8e46a..ff1e7080d 100644 --- a/tests/app/main/views/test_jobs_activity.py +++ b/tests/app/main/views/test_jobs_activity.py @@ -50,6 +50,9 @@ def test_all_activity( mock_get_page_of_jobs = mocker.patch( "app.job_api_client.get_page_of_jobs", return_value=MOCK_JOBS ) + mocker.patch( + "app.job_api_client.get_immediate_jobs", return_value=[] + ) response = client_request.get_response( "main.all_jobs_activity", @@ -60,7 +63,11 @@ def test_all_activity( assert response.data is not None, "Response data is None" assert "All activity" in response.text - mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) + + assert any( + call[0][0] == SERVICE_ONE_ID and call[1].get('page') == current_page + for call in mock_get_page_of_jobs.call_args_list + ) page = BeautifulSoup(response.data, "html.parser") table = page.find("table") assert table is not None, "Table not found in the response" @@ -115,7 +122,6 @@ def test_all_activity( failed_cell = cells[6].get_text(strip=True) assert failed_cell == "5", f"Expected failed count '5', but got '{failed_cell}'" - mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) def test_all_activity_no_jobs(client_request, mocker): @@ -133,6 +139,9 @@ def test_all_activity_no_jobs(client_request, mocker): "total": 0, }, ) + mocker.patch( + "app.job_api_client.get_immediate_jobs", return_value=[] + ) response = client_request.get_response( "main.all_jobs_activity", service_id=SERVICE_ONE_ID, @@ -152,7 +161,10 @@ def test_all_activity_no_jobs(client_request, mocker): assert ( expected_message == actual_message ), f"Expected message '{expected_message}', but got '{actual_message}'" - mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) + assert any( + call[0][0] == SERVICE_ONE_ID and call[1].get('page') == current_page + for call in mock_get_page_of_jobs.call_args_list + ) def test_all_activity_pagination(client_request, mocker): @@ -182,13 +194,19 @@ def test_all_activity_pagination(client_request, mocker): "total": 100, }, ) + mocker.patch( + "app.job_api_client.get_immediate_jobs", return_value=[] + ) response = client_request.get_response( "main.all_jobs_activity", service_id=SERVICE_ONE_ID, page=current_page, ) - mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) + assert any( + call[0][0] == SERVICE_ONE_ID and call[1].get('page') == current_page + for call in mock_get_page_of_jobs.call_args_list + ) page = BeautifulSoup(response.data, "html.parser") pagination_controls = page.find_all("li", class_="usa-pagination__item") From e5885f674b35e1c03630ec4e4fdfabe5d1146699 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Tue, 29 Jul 2025 10:25:43 -0700 Subject: [PATCH 05/10] Fixed pagination logic to prevent infinite next button --- app/main/views/activity.py | 4 +++- app/main/views/jobs.py | 3 +++ tests/app/main/views/test_activity.py | 31 ++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/main/views/activity.py b/app/main/views/activity.py index 9dd60b35b..1d6d12d98 100644 --- a/app/main/views/activity.py +++ b/app/main/views/activity.py @@ -97,10 +97,12 @@ def handle_pagination(jobs, service_id, page): else None ) total_items = jobs.get("total", 0) + page_size = jobs.get("page_size", 50) + total_pages = (total_items + page_size - 1) // page_size has_next_link = jobs.get("links", {}).get("next") is not None next_page = ( generate_next_dict("main.all_jobs_activity", service_id, page) - if has_next_link and total_items > 50 + if has_next_link and total_items > 50 and page < total_pages else None ) pagination = generate_pagination_pages( diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index 938a0849d..afe0504bf 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -254,10 +254,13 @@ def get_notifications(service_id, message_type, status_override=None): # noqa next_page = None total_items = notifications.get("total", 0) + page_size = notifications.get("page_size", 50) + total_pages = (total_items + page_size - 1) // page_size if ( "links" in notifications and notifications["links"].get("next", None) and total_items > 50 + and page < total_pages ): next_page = generate_next_dict( "main.view_notifications", service_id, page, url_args diff --git a/tests/app/main/views/test_activity.py b/tests/app/main/views/test_activity.py index 9fd5118db..b9dc3d94a 100644 --- a/tests/app/main/views/test_activity.py +++ b/tests/app/main/views/test_activity.py @@ -562,7 +562,7 @@ def test_should_show_notifications_for_a_service_with_next_previous( "app.notification_api_client.get_notifications_for_service", return_value=notification_json( service_one["id"], rows=50, with_links=True - ) | {"total": 100}, + ) | {"total": 150}, ) page = client_request.get( "main.view_notifications", @@ -597,6 +597,35 @@ def test_should_show_notifications_for_a_service_with_next_previous( assert "page 1" in prev_page_link.text.strip() +def test_doesnt_show_next_button_on_last_page( + client_request, + service_one, + active_user_with_permissions, + mock_get_service_statistics, + mock_get_service_data_retention, + mock_get_no_api_keys, + mocker, +): + mocker.patch( + "app.notification_api_client.get_notifications_for_service", + return_value=notification_json( + service_one["id"], rows=50, with_links=True + ) | {"total": 100}, + ) + page = client_request.get( + "main.view_notifications", + service_id=service_one["id"], + message_type="sms", + page=2, + ) + + next_page_link = page.find("a", {"rel": "next"}) + prev_page_link = page.find("a", {"rel": "previous"}) + + assert next_page_link is None + assert prev_page_link is not None + + def test_doesnt_show_pagination_when_50_or_fewer_items( client_request, service_one, From f3df2f9c33d4c330d6594b11c0b0d3d677b61aff Mon Sep 17 00:00:00 2001 From: Carlo Costino Date: Wed, 30 Jul 2025 12:24:16 -0400 Subject: [PATCH 06/10] Downgrade Python to 3.12.9 again This changeset puts us back to Python 3.12.9 since we are still wrestling with certificate validation errors in Python 3.13 and no easy way to test things without breaking our deployment flow currently. Signed-off-by: Carlo Costino --- .github/actions/setup-project/action.yml | 4 ++-- README.md | 16 +++++++------- poetry.lock | 27 ++++++++++++++++++++++-- pyproject.toml | 2 +- runtime.txt | 2 +- 5 files changed, 37 insertions(+), 14 deletions(-) diff --git a/.github/actions/setup-project/action.yml b/.github/actions/setup-project/action.yml index ad207f112..de142ed6c 100644 --- a/.github/actions/setup-project/action.yml +++ b/.github/actions/setup-project/action.yml @@ -9,10 +9,10 @@ runs: sudo apt-get update \ && sudo apt-get install -y --no-install-recommends \ libcurl4-openssl-dev - - name: Set up Python 3.13.2 + - name: Set up Python 3.12.9 uses: actions/setup-python@v4 with: - python-version: "3.13.2" + python-version: "3.12.9" - name: Install poetry shell: bash run: pip install poetry==2.1.3 diff --git a/README.md b/README.md index e30269dff..8ed57ef2a 100644 --- a/README.md +++ b/README.md @@ -189,12 +189,12 @@ session to make the changes take effect. Now we're ready to install the Python version we need with `pyenv`, like so: ```sh -pyenv install 3.13 +pyenv install 3.12 ``` -This will install the latest version of Python 3.13. +This will install the latest version of Python 3.12. -_NOTE: This project currently runs on Python 3.13.x._ +_NOTE: This project currently runs on Python 3.12.x._ #### [API Step] Python Dependency Installation @@ -246,12 +246,12 @@ git clone git@github.com:GSA/notifications-admin.git Now go into the project directory (`notifications-admin` by default), create a virtual environment, and set the local Python version to point to the virtual -environment (assumes version Python `3.13.2` is what is installed on your +environment (assumes version Python `3.12.9` is what is installed on your machine): ```sh cd notifications-admin -pyenv virtualenv 3.13.2 notify-admin +pyenv virtualenv 3.12.9 notify-admin pyenv local notify-admin ``` @@ -284,10 +284,10 @@ If you're upgrading an existing project to a newer version of Python, you can follow these steps to get yourself up-to-date. First, use `pyenv` to install the newer version of Python you'd like to use; -we'll use `3.13` in our example here since we recently upgraded to this version: +we'll use `3.12` in our example here since we recently upgraded to this version: ```sh -pyenv install 3.13 +pyenv install 3.12 ``` Next, delete the virtual environment you previously had set up. If you followed @@ -302,7 +302,7 @@ environment with the newer version of Python you just installed: ```sh cd notifications-admin -pyenv virtualenv 3.13.2 notify-admin +pyenv virtualenv 3.12.9 notify-admin pyenv local notify-admin ``` diff --git a/poetry.lock b/poetry.lock index 3300b1563..b13eb0aab 100644 --- a/poetry.lock +++ b/poetry.lock @@ -27,6 +27,7 @@ files = [ [package.dependencies] idna = ">=2.8" sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] @@ -929,6 +930,9 @@ files = [ {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + [package.extras] test = ["pytest (>=6)"] @@ -1713,6 +1717,8 @@ files = [ {file = "lxml-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:35bc626eec405f745199200ccb5c6b36f202675d204aa29bb52e27ba2b71dea8"}, {file = "lxml-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:246b40f8a4aec341cbbf52617cad8ab7c888d944bfe12a6abd2b1f6cfb6f6082"}, {file = "lxml-6.0.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2793a627e95d119e9f1e19720730472f5543a6d84c50ea33313ce328d870f2dd"}, + {file = "lxml-6.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:46b9ed911f36bfeb6338e0b482e7fe7c27d362c52fde29f221fddbc9ee2227e7"}, + {file = "lxml-6.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b4790b558bee331a933e08883c423f65bbcd07e278f91b2272489e31ab1e2b4"}, {file = "lxml-6.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2030956cf4886b10be9a0285c6802e078ec2391e1dd7ff3eb509c2c95a69b76"}, {file = "lxml-6.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d23854ecf381ab1facc8f353dcd9adeddef3652268ee75297c1164c987c11dc"}, {file = "lxml-6.0.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:43fe5af2d590bf4691531b1d9a2495d7aab2090547eaacd224a3afec95706d76"}, @@ -1725,6 +1731,8 @@ files = [ {file = "lxml-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ee56288d0df919e4aac43b539dd0e34bb55d6a12a6562038e8d6f3ed07f9e36"}, {file = "lxml-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8dd6dd0e9c1992613ccda2bcb74fc9d49159dbe0f0ca4753f37527749885c25"}, {file = "lxml-6.0.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d7ae472f74afcc47320238b5dbfd363aba111a525943c8a34a1b657c6be934c3"}, + {file = "lxml-6.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5592401cdf3dc682194727c1ddaa8aa0f3ddc57ca64fd03226a430b955eab6f6"}, + {file = "lxml-6.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58ffd35bd5425c3c3b9692d078bf7ab851441434531a7e517c4984d5634cd65b"}, {file = "lxml-6.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f720a14aa102a38907c6d5030e3d66b3b680c3e6f6bc95473931ea3c00c59967"}, {file = "lxml-6.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a5e8d207311a0170aca0eb6b160af91adc29ec121832e4ac151a57743a1e1e"}, {file = "lxml-6.0.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:2dd1cc3ea7e60bfb31ff32cafe07e24839df573a5e7c2d33304082a5019bcd58"}, @@ -1737,11 +1745,15 @@ files = [ {file = "lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108"}, {file = "lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be"}, {file = "lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7"}, {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991"}, {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da"}, {file = "lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e"}, {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741"}, {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3"}, + {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16"}, {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0"}, {file = "lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a"}, {file = "lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3"}, @@ -1749,17 +1761,22 @@ files = [ {file = "lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da"}, {file = "lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7"}, {file = "lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1"}, {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a"}, {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29"}, {file = "lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4"}, {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca"}, {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf"}, + {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f"}, {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef"}, {file = "lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181"}, {file = "lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e"}, {file = "lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03"}, {file = "lxml-6.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4eb114a0754fd00075c12648d991ec7a4357f9cb873042cc9a77bf3a7e30c9db"}, {file = "lxml-6.0.0-cp38-cp38-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:7da298e1659e45d151b4028ad5c7974917e108afb48731f4ed785d02b6818994"}, + {file = "lxml-6.0.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bf61bc4345c1895221357af8f3e89f8c103d93156ef326532d35c707e2fb19d"}, {file = "lxml-6.0.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63b634facdfbad421d4b61c90735688465d4ab3a8853ac22c76ccac2baf98d97"}, {file = "lxml-6.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e380e85b93f148ad28ac15f8117e2fd8e5437aa7732d65e260134f83ce67911b"}, {file = "lxml-6.0.0-cp38-cp38-win32.whl", hash = "sha256:185efc2fed89cdd97552585c624d3c908f0464090f4b91f7d92f8ed2f3b18f54"}, @@ -1767,6 +1784,8 @@ files = [ {file = "lxml-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85b14a4689d5cff426c12eefe750738648706ea2753b20c2f973b2a000d3d261"}, {file = "lxml-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f64ccf593916e93b8d36ed55401bb7fe9c7d5de3180ce2e10b08f82a8f397316"}, {file = "lxml-6.0.0-cp39-cp39-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:b372d10d17a701b0945f67be58fae4664fd056b85e0ff0fbc1e6c951cdbc0512"}, + {file = "lxml-6.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a674c0948789e9136d69065cc28009c1b1874c6ea340253db58be7622ce6398f"}, + {file = "lxml-6.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:edf6e4c8fe14dfe316939711e3ece3f9a20760aabf686051b537a7562f4da91a"}, {file = "lxml-6.0.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:048a930eb4572829604982e39a0c7289ab5dc8abc7fc9f5aabd6fbc08c154e93"}, {file = "lxml-6.0.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0b5fa5eda84057a4f1bbb4bb77a8c28ff20ae7ce211588d698ae453e13c6281"}, {file = "lxml-6.0.0-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:c352fc8f36f7e9727db17adbf93f82499457b3d7e5511368569b4c5bd155a922"}, @@ -1777,10 +1796,14 @@ files = [ {file = "lxml-6.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:e0b1520ef900e9ef62e392dd3d7ae4f5fa224d1dd62897a792cf353eb20b6cae"}, {file = "lxml-6.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:e35e8aaaf3981489f42884b59726693de32dabfc438ac10ef4eb3409961fd402"}, {file = "lxml-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:dbdd7679a6f4f08152818043dbb39491d1af3332128b3752c3ec5cebc0011a72"}, + {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40442e2a4456e9910875ac12951476d36c0870dcb38a68719f8c4686609897c4"}, + {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db0efd6bae1c4730b9c863fc4f5f3c0fa3e8f05cae2c44ae141cb9dfc7d091dc"}, {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ab542c91f5a47aaa58abdd8ea84b498e8e49fe4b883d67800017757a3eb78e8"}, {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:013090383863b72c62a702d07678b658fa2567aa58d373d963cca245b017e065"}, {file = "lxml-6.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c86df1c9af35d903d2b52d22ea3e66db8058d21dc0f59842ca5deb0595921141"}, {file = "lxml-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4337e4aec93b7c011f7ee2e357b0d30562edd1955620fdd4aeab6aacd90d43c5"}, + {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ae74f7c762270196d2dda56f8dd7309411f08a4084ff2dfcc0b095a218df2e06"}, + {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:059c4cbf3973a621b62ea3132934ae737da2c132a788e6cfb9b08d63a0ef73f9"}, {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f090a9bc0ce8da51a5632092f98a7e7f84bca26f33d161a98b57f7fb0004ca"}, {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9da022c14baeec36edfcc8daf0e281e2f55b950249a455776f0d1adeeada4734"}, {file = "lxml-6.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a55da151d0b0c6ab176b4e761670ac0e2667817a1e0dadd04a01d0561a219349"}, @@ -4206,5 +4229,5 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" -python-versions = "^3.13.2" -content-hash = "6f5a26be48841af3784f06d9c79c27a4bc7c2c8183da5d27eae2d371a2884258" +python-versions = "^3.12.9" +content-hash = "073234a8e1c1abf220f2591459e31fe3c45e87eda57f1dc4fc64804d79051ef7" diff --git a/pyproject.toml b/pyproject.toml index f4a78cbac..dd07f524d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ package-mode = false [tool.poetry.dependencies] axe-core-python = "^0.1.0" -python = "^3.13.2" +python = "^3.12.9" ago = "~=0.1.0" beautifulsoup4 = "^4.13.3" blinker = "~=1.8" diff --git a/runtime.txt b/runtime.txt index c94f67640..64f28603a 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.13.x +python-3.12.x From e82e138771fe402bcfc55111730d3202635c0dc0 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 30 Jul 2025 10:51:54 -0700 Subject: [PATCH 07/10] change gunicorn timeout to 240 seconds --- Makefile | 2 +- gunicorn_config.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 97cd2d682..8684a22e2 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ watch-frontend: ## Build frontend and watch for changes .PHONY: run-flask run-flask: ## Run flask - poetry run newrelic-admin run-program flask run -p 6012 --host=0.0.0.0 + poetry run newrelic-admin run-program gunicorn -c gunicorn_config.py gunicorn_entry:application -b 0.0.0.0:6012 .PHONY: run-flask-bare run-flask-bare: ## Run flask without invoking poetry so we can override ENV variables in .env diff --git a/gunicorn_config.py b/gunicorn_config.py index 4971de009..2643f78c9 100644 --- a/gunicorn_config.py +++ b/gunicorn_config.py @@ -2,7 +2,8 @@ import multiprocessing import os import sys import traceback - +import logging +from app.utils import hilite import gunicorn # Let gunicorn figure out the right number of workers @@ -11,6 +12,10 @@ import gunicorn # so adjust it. workers = multiprocessing.cpu_count() worker_class = "gevent" + +logging.basicConfig(level=logging.INFO) +logging.info(hilite("Gunicorn timeout set to 240 seconds")) +timeout = 240 bind = "0.0.0.0:{}".format(os.getenv("PORT")) disable_redirect_access_to_syslog = True gunicorn.SERVER_SOFTWARE = "None" From d35c7bbd5f5b969abd111f1f8133faa21f5f2139 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 30 Jul 2025 11:15:21 -0700 Subject: [PATCH 08/10] temporarily weaken axe checks --- tests/end_to_end/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/end_to_end/conftest.py b/tests/end_to_end/conftest.py index eb90086ce..ed8c28858 100644 --- a/tests/end_to_end/conftest.py +++ b/tests/end_to_end/conftest.py @@ -48,6 +48,7 @@ def check_axe_report(page): assert violation["impact"] in [ "minor", "moderate", + "serious", # TODO!!! REMOVE THIS ], f"Accessibility violation: {violation}" From 4c258bb39b261cece71c757a645e058e04e4b1e5 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 30 Jul 2025 11:25:36 -0700 Subject: [PATCH 09/10] ugh --- Makefile | 2 +- tests/end_to_end/conftest.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8684a22e2..1e6eeefb9 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ watch-frontend: ## Build frontend and watch for changes .PHONY: run-flask run-flask: ## Run flask - poetry run newrelic-admin run-program gunicorn -c gunicorn_config.py gunicorn_entry:application -b 0.0.0.0:6012 + poetry run newrelic-admin run-program flask run -p 6012 --host=0.0.0.0 .PHONY: run-flask-bare run-flask-bare: ## Run flask without invoking poetry so we can override ENV variables in .env diff --git a/tests/end_to_end/conftest.py b/tests/end_to_end/conftest.py index ed8c28858..eb90086ce 100644 --- a/tests/end_to_end/conftest.py +++ b/tests/end_to_end/conftest.py @@ -48,7 +48,6 @@ def check_axe_report(page): assert violation["impact"] in [ "minor", "moderate", - "serious", # TODO!!! REMOVE THIS ], f"Accessibility violation: {violation}" From 1a5c56b621cf38e17b90c78625967a3bdfaae080 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 30 Jul 2025 11:31:22 -0700 Subject: [PATCH 10/10] ugh --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1e6eeefb9..97cd2d682 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ watch-frontend: ## Build frontend and watch for changes .PHONY: run-flask run-flask: ## Run flask - poetry run newrelic-admin run-program flask run -p 6012 --host=0.0.0.0 + poetry run newrelic-admin run-program flask run -p 6012 --host=0.0.0.0 .PHONY: run-flask-bare run-flask-bare: ## Run flask without invoking poetry so we can override ENV variables in .env