diff --git a/.ds.baseline b/.ds.baseline index dde326a7c..485ff9443 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -629,10 +629,10 @@ "filename": "tests/conftest.py", "hashed_secret": "f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea", "is_verified": false, - "line_number": 3267, + "line_number": 3279, "is_secret": false } ] }, - "generated_at": "2025-07-31T21:07:14Z" + "generated_at": "2025-08-07T18:05:06Z" } diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index b4ece1215..3d13cf505 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -101,7 +101,8 @@ jobs: # above make e2e-test run: | - make run-flask-and-wait > admin-server.log 2>&1 & + make run-flask > admin-server.log 2>&1 & + make wait-for-flask tail -f admin-server.log & make e2e-test diff --git a/Makefile b/Makefile index 7d52529ea..9de9b6492 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,12 @@ watch-frontend: ## Build frontend and watch for changes run-flask: ## Run flask poetry run newrelic-admin run-program flask run -p 6012 --host=0.0.0.0 +.PHONY: wait-for-flask +wait-for-flask: + @echo "Waiting for Flask to start..." + @timeout 30 bash -c 'until curl -sf http://localhost:6012 > /dev/null 2>&1; do sleep 1; done' + @echo "Flask is ready!" + .PHONY: run-flask-and-wait run-flask-and-wait: @make run-flask & diff --git a/app/assets/javascripts/activityChart.js b/app/assets/javascripts/activityChart.js index 4fa8922ec..be15a3aad 100644 --- a/app/assets/javascripts/activityChart.js +++ b/app/assets/javascripts/activityChart.js @@ -295,24 +295,28 @@ liveRegion.textContent = `Data updated for ${selectedText} - last 7 days`; const tableHeading = document.querySelector('#tableActivity h2'); - const senderColumns = document.querySelectorAll('.sender-column'); + const senderElements = [ + document.querySelector('[data-column="sender"]'), + ...document.querySelectorAll('[data-sender]') + ]; const allRows = document.querySelectorAll('#activity-table tbody tr'); - const caption = document.querySelector('#activity-table caption'); + const table = document.getElementById('activity-table'); + const caption = table.querySelector('caption'); if (selectedValue === 'individual') { tableHeading.textContent = 'My activity'; caption.textContent = `Table showing the sent jobs for ${currentUserName}`; - senderColumns.forEach(col => { - col.style.display = 'none'; + senderElements.forEach(el => { + if (el) el.style.display = 'none'; }); allRows.forEach(row => row.style.display = 'none'); const userRows = Array.from(allRows).filter(row => { - const senderCell = row.querySelector('.sender-column'); - const rowSender = senderCell ? senderCell.textContent.trim() : ''; + const senderCell = row.querySelector('[data-sender]'); + const rowSender = senderCell ? senderCell.dataset.sender : ''; return rowSender === currentUserName; }); @@ -333,8 +337,8 @@ tableHeading.textContent = 'Service activity'; caption.textContent = `Table showing the sent jobs for service`; - senderColumns.forEach(col => { - col.style.display = ''; + senderElements.forEach(el => { + if (el) el.style.display = ''; }); allRows.forEach((row, index) => { diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index 029aaa22f..9aebabd35 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -492,44 +492,12 @@ td.table-empty-message { .job-table { width: 100%; border-collapse: collapse; - td.file-name { - width: 25%; - overflow-wrap: anywhere; - } - td.jobid { - width: 5%; - } - td.template { - width: 25%; - } - td.time-sent { - width: 15%; - } - td.sender { - width: 20%; - overflow-wrap: break-word; - } - td.count-of-recipients { - width: 5%; - } - td.report { - width: 2%; - text-align: center; - } - td.delivered { - width: 2%; - text-align: center; - } - td.failed { - width: 2%; - text-align: center; - } - td.report img { - padding-top: 5px; - } th { padding: 0.5rem 1rem; } + td { + padding: 0.5rem 1rem; + } } @media (max-width: 768px) { diff --git a/app/main/views/activity.py b/app/main/views/activity.py index d1bddf351..43f51dfa7 100644 --- a/app/main/views/activity.py +++ b/app/main/views/activity.py @@ -73,6 +73,7 @@ def all_jobs_activity(service_id): next_page=next_page, prev_page=prev_page, pagination=pagination, + total_jobs=jobs.get("total", 0), **download_availability, download_link_one_day=url_for( ".download_notifications_csv", diff --git a/app/templates/views/activity/all-activity.html b/app/templates/views/activity/all-activity.html index 675b7c478..2a3daac97 100644 --- a/app/templates/views/activity/all-activity.html +++ b/app/templates/views/activity/all-activity.html @@ -51,6 +51,11 @@ {% endif %} + {% if pagination and total_jobs %} +

+ Page {{ pagination.current }} of {{ pagination.last }} ({{ total_jobs }} total jobs) +

+ {% endif %} {% endif %} {% endset %} {% block maincolumn_content %} @@ -88,7 +93,7 @@ -
+
@@ -96,23 +101,20 @@ - - - - - - @@ -120,28 +122,34 @@ {% if all_jobs_dict %} {% for job in all_jobs_dict %} - - - + - - + - - + {% endfor %} {% else %} @@ -152,7 +160,7 @@
Table showing all sent jobs for this service
Job ID# + Template + Started + Sender + Report - Delivered - - Failed + + Status
+ {{ job.job_id[:8] if job.job_id else 'Manually entered number' }} {{ job.template_name }} + {{ job.template_name }} {{ job.activity_time|format_datetime_table }} {{ job.created_by.name }} + {{ job.created_by.name }} {% if job.can_download %} - + Download report link {% else %} N/A {% endif %} {{ job.delivered_count if job.delivered_count is not none else '0' }}{{ job.failed_count if job.failed_count is not none else '0' }} + + {{ job.delivered_count if job.delivered_count is not none else '0' }} delivered + + + {{ job.failed_count if job.failed_count is not none else '0' }} failed + +
-

Note: Report data is only available for 7 days after your message has been sent

+

Note: Report data is only available for 7 days after your message has been sent

{{show_pagination}} {% if current_user.has_permissions(ServicePermission.VIEW_ACTIVITY) %} diff --git a/app/templates/views/dashboard/activity-table.html b/app/templates/views/dashboard/activity-table.html index 1dbb2bc7d..9205c5228 100644 --- a/app/templates/views/dashboard/activity-table.html +++ b/app/templates/views/dashboard/activity-table.html @@ -22,15 +22,15 @@

Service activity

- +
- - + @@ -39,13 +39,13 @@ {% if jobs %} {% for job in jobs %} - - - + - - + + {% endfor %} {% else %} diff --git a/backstop_data/bitmaps_reference/backstop_test_All_Activity_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_All_Activity_0_document_0_desktop.png new file mode 100644 index 000000000..7dc758fa1 Binary files /dev/null and b/backstop_data/bitmaps_reference/backstop_test_All_Activity_0_document_0_desktop.png differ diff --git a/poetry.lock b/poetry.lock index a06d6fb17..879be20eb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -339,14 +339,14 @@ files = [ [[package]] name = "certifi" -version = "2025.7.14" +version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2"}, - {file = "certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995"}, + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, ] [[package]] @@ -4223,4 +4223,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "^3.12.9" -content-hash = "9c4815ed8afb0be6324db1a3a04aaaff93cc697af6be0b9d5bf272bec2f3aee8" +content-hash = "13f71f1bead8fec7d973a07f47da9a423f15f642eb0309212a019bb8865651f1" diff --git a/pyproject.toml b/pyproject.toml index 755a1ea85..40d110416 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ regex = "^2025.7.34" s3transfer = "^0.10.2" shapely = "^2.1.1" smartypants = "^2.0.1" -certifi = "^2025.7.14" +certifi = "^2025.8.3" charset-normalizer = "^3.4.2" click = "^8.2.1" idna = "^3.7" diff --git a/tests/app/main/views/test_jobs_activity.py b/tests/app/main/views/test_jobs_activity.py index 305d9fdeb..53d50d56f 100644 --- a/tests/app/main/views/test_jobs_activity.py +++ b/tests/app/main/views/test_jobs_activity.py @@ -78,8 +78,7 @@ def test_all_activity( "Started", "Sender", "Report", - "Delivered", - "Failed", + "Status", ] assert ( @@ -91,7 +90,7 @@ def test_all_activity( job_row = rows[0] cells = job_row.find_all("td") - assert len(cells) == 7, "Expected five columns in the job row" + assert len(cells) == 6, "Expected six columns in the job row" job_id_cell = cells[0].find("a").get_text(strip=True) @@ -114,13 +113,9 @@ def test_all_activity( report_cell = cells[4].find("span").get_text(strip=True) assert report_cell == "N/A", f"Expected report 'N/A', but got '{report_cell}'" - delivered_cell = cells[5].get_text(strip=True) - assert ( - delivered_cell == "1" - ), f"Expected delivered count '1', but got '{delivered_cell}'" - - failed_cell = cells[6].get_text(strip=True) - assert failed_cell == "5", f"Expected failed count '5', but got '{failed_cell}'" + status_cell = cells[5].get_text(strip=True) + assert "1 delivered" in status_cell, f"Expected status to contain '1 delivered', but got '{status_cell}'" + assert "5 failed" in status_cell, f"Expected status to contain '5 failed', but got '{status_cell}'" def test_all_activity_no_jobs(client_request, mocker): diff --git a/tests/conftest.py b/tests/conftest.py index cba945a42..2174cc4b7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -556,6 +556,18 @@ def mock_update_service_raise_httperror_duplicate_name(mocker): return mocker.patch("app.service_api_client.update_service", side_effect=_update) +@pytest.fixture(autouse=True) +def _disable_e2e_mode_for_unit_tests(monkeypatch, request): + """ + Disable E2E test mode for all unit tests to prevent unmocked API calls. + E2E tests are run in a separate job and should not interfere with unit tests. + """ + # Only disable if unit tests are not being run + if "end_to_end" not in request.node.nodeid: + monkeypatch.delenv("NOTIFY_E2E_TEST_EMAIL", raising=False) + monkeypatch.delenv("NOTIFY_E2E_TEST_PASSWORD", raising=False) + + SERVICE_ONE_ID = "596364a0-858e-42c8-9062-a8fe822260eb" SERVICE_TWO_ID = "147ad62a-2951-4fa1-9ca0-093cd1a52c52" ORGANISATION_ID = "c011fa40-4cbe-4524-b415-dde2f421bd9c" diff --git a/tests/javascripts/activityChart.test.js b/tests/javascripts/activityChart.test.js index 89e924dc1..ff90d3342 100644 --- a/tests/javascripts/activityChart.test.js +++ b/tests/javascripts/activityChart.test.js @@ -239,14 +239,19 @@ test('handleDropdownChange updates DOM for individual selection', () => {

Table showing the sent jobs for {{current_service.name}}
Job ID# TemplateJob statusSender + Job statusSender # of Recipients
+ {{ job.id[:8] if job.id else 'Manually entered number' }} {{ job.template_name }} + {{ job.template_name }} {% if not job.finished_processing %} {% if job.scheduled_for%} Scheduled for {{ job.scheduled_for|format_datetime_table }} @@ -58,8 +58,8 @@ Sent on {{ job.processing_started|format_datetime_table }} {% endif %} {{ job.created_by.name }}{{ job.notification_count }}{{ job.created_by.name }}{{ job.notification_count }}
+ + + + + - - - - - - - + + + + + + +
Sender
Test User
Other User
Test User
Test User
Other User
Test User
Test User
Test User
Other User
Test User
Test User
Other User
Test User
Test User
@@ -269,15 +274,18 @@ test('handleDropdownChange updates DOM for individual selection', () => { expect(document.getElementById('table-heading').textContent).toBe('My activity'); expect(document.getElementById('caption').textContent).toContain('Test User'); - document.querySelectorAll('.sender-column').forEach(col => { - expect(col.style.display).toBe('none'); + const senderHeader = document.querySelector('[data-column="sender"]'); + expect(senderHeader.style.display).toBe('none'); + + document.querySelectorAll('[data-sender]').forEach(cell => { + expect(cell.style.display).toBe('none'); }); const rows = Array.from(document.querySelectorAll('#activity-table tbody tr')); const visibleRows = rows.filter(row => row.style.display !== 'none'); expect(visibleRows.length).toBeLessThanOrEqual(5); visibleRows.forEach(row => { - const sender = row.querySelector('.sender-column').textContent.trim(); + const sender = row.querySelector('[data-sender]').dataset.sender; expect(sender).toBe('Test User'); }); @@ -295,10 +303,15 @@ test('handleDropdownChange shows empty message when user has no jobs', () => {

+ + + + + - - - + + + @@ -324,8 +337,11 @@ test('handleDropdownChange shows empty message when user has no jobs', () => { expect(document.getElementById('table-heading').textContent).toBe('My activity'); expect(document.getElementById('caption').textContent).toContain('Test User'); - document.querySelectorAll('.sender-column').forEach(col => { - expect(col.style.display).toBe('none'); + const senderHeader = document.querySelector('[data-column="sender"]'); + expect(senderHeader.style.display).toBe('none'); + + document.querySelectorAll('[data-sender]').forEach(cell => { + expect(cell.style.display).toBe('none'); }); const emptyMessageRow = document.querySelector('.table-empty-message').closest('tr'); diff --git a/urls.js b/urls.js index c53398bb9..f9ad9bd8a 100644 --- a/urls.js +++ b/urls.js @@ -67,6 +67,10 @@ const routes = { label: 'Team Members', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/users', }, + { + label: 'All Activity', + path: '/activity/services/9c765540-266e-474e-b6bb-8e2e0e32b781', + }, { label: 'Invite User', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/users/invite',
Sender
Other User
Another User
Different User
Other User
Another User
Different User
No batched job messages found (messages are kept for 7 days).