Merge branch 'main' of https://github.com/GSA/notifications-admin into 1669-toggle-between-recent-activity-and-all-activity

This commit is contained in:
Jonathan Bobel
2024-08-05 09:34:50 -04:00
20 changed files with 523 additions and 59 deletions

View File

@@ -414,6 +414,30 @@ td.table-empty-message {
} }
} }
.job-status-table {
table-layout: fixed;
thead tr th {
border-bottom: 0;
}
thead,
tbody,
tr {
width: 100%;
}
th:first-child,
td:first-child {
width: 75%;
}
th:nth-child(2),R
td:nth-child(2) {
width: 25%;
}
}
.usage-table { .usage-table {
ul { ul {
list-style: none; list-style: none;
@@ -433,27 +457,43 @@ td.table-empty-message {
width: 25%; width: 25%;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
td.jobid {
width: 5%;
}
td.template { td.template {
width: 20%; width: 25%;
} }
td.time-sent { td.time-sent {
width: 20%; width: 30%;
} }
td.sender { td.sender {
width: 15%; width: 20%;
overflow-wrap: break-word;
} }
td.count-of-recipients { td.count-of-recipients {
width: 5%; width: 5%;
} }
td.report { td.report {
width: 5%; width: 5%;
text-align: center;
} }
td.report img {
padding-top: 5px;
}
th { th {
padding: 0.5rem 1rem padding: 0.5rem 1rem
} }
td { }
padding: 0.5rem 1rem
} @media (max-width: 768px) {
.usa-table-container--scrollable-mobile {
margin: 0;
overflow-y:hidden;
}
}
.usa-table th[data-sortable][aria-sort=ascending], .usa-table th[data-sortable][aria-sort=descending] {
background-color: #a1d3ff;
} }
#template-list { #template-list {
@@ -468,6 +508,10 @@ td.table-empty-message {
} }
} }
.usa-prose > p.max-width-full {
max-width: 100%;
}
// Tabs // Tabs
.tabs { .tabs {

View File

@@ -231,6 +231,11 @@ def naturaltime_without_indefinite_article(date):
) )
def convert_time_unixtimestamp(date_string):
dt = datetime.fromisoformat(date_string)
return int(dt.timestamp())
def format_delta(date): def format_delta(date):
# This method assumes that date is in UTC # This method assumes that date is in UTC
date = parse_naive_dt(date) date = parse_naive_dt(date)

View File

@@ -3,6 +3,7 @@ from flask import Blueprint
main = Blueprint("main", __name__) main = Blueprint("main", __name__)
from app.main.views import ( # noqa isort:skip from app.main.views import ( # noqa isort:skip
activity,
add_service, add_service,
api_keys, api_keys,
choose_account, choose_account,

View File

@@ -0,0 +1,80 @@
from flask import abort, render_template, request, url_for
from app import current_service, job_api_client
from app.formatters import convert_time_unixtimestamp, get_time_left
from app.main import main
from app.utils.pagination import (
generate_next_dict,
generate_pagination_pages,
generate_previous_dict,
get_page_from_request,
)
from app.utils.user import user_has_permissions
@main.route("/activity/services/<uuid:service_id>")
@user_has_permissions()
def all_jobs_activity(service_id):
service_data_retention_days = 7
page = get_page_from_request()
jobs = job_api_client.get_page_of_jobs(service_id, page=page)
all_jobs_dict = generate_job_dict(jobs)
prev_page, next_page, pagination = handle_pagination(jobs, service_id, page)
return render_template(
"views/activity/all-activity.html",
all_jobs_dict=all_jobs_dict,
service_data_retention_days=service_data_retention_days,
next_page=next_page,
prev_page=prev_page,
pagination=pagination,
)
def handle_pagination(jobs, service_id, page):
if page is None:
abort(404, "Invalid page argument ({}).".format(request.args.get("page")))
prev_page = (
generate_previous_dict("main.all_jobs_activity", service_id, page)
if page > 1
else None
)
next_page = (
generate_next_dict("main.all_jobs_activity", service_id, page)
if jobs.get("links", {}).get("next")
else None
)
pagination = generate_pagination_pages(
jobs.get("total", {}), jobs.get("page_size", {}), page
)
return prev_page, next_page, pagination
def generate_job_dict(jobs):
return [
{
"job_id": job["id"],
"time_left": get_time_left(job["created_at"]),
"download_link": url_for(
".view_job_csv", service_id=current_service.id, job_id=job["id"]
),
"view_job_link": url_for(
".view_job", service_id=current_service.id, job_id=job["id"]
),
"created_at": job["created_at"],
"time_sent_data_value": convert_time_unixtimestamp(
job["processing_finished"]
if job["processing_finished"]
else (
job["processing_started"]
if job["processing_started"]
else job["created_at"]
)
),
"processing_finished": job["processing_finished"],
"processing_started": job["processing_started"],
"created_by": job["created_by"],
"template_name": job["template_name"],
}
for job in jobs["data"]
]

View File

@@ -153,6 +153,9 @@ class HeaderNavigation(Navigation):
class MainNavigation(Navigation): class MainNavigation(Navigation):
mapping = { mapping = {
"activity": {
"all_jobs_activity",
},
"dashboard": { "dashboard": {
"conversation", "conversation",
"inbox", "inbox",

View File

@@ -1,6 +1,8 @@
import datetime import datetime
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from flask import current_app
from app.extensions import redis_client from app.extensions import redis_client
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
from app.utils.csv import get_user_preferred_timezone from app.utils.csv import get_user_preferred_timezone
@@ -27,9 +29,7 @@ class JobApiClient(NotifyAdminAPIClient):
def get_job(self, service_id, job_id): def get_job(self, service_id, job_id):
params = {} params = {}
job = self.get( job = self.get(url=f"/service/{service_id}/job/{job_id}", params=params)
url="/service/{}/job/{}".format(service_id, job_id), params=params
)
return job return job
@@ -40,13 +40,16 @@ class JobApiClient(NotifyAdminAPIClient):
if statuses is not None: if statuses is not None:
params["statuses"] = ",".join(statuses) params["statuses"] = ",".join(statuses)
return self.get(url="/service/{}/job".format(service_id), params=params) job = self.get(url=f"/service/{service_id}/job", params=params)
from pprint import pformat
current_app.logger.info(pformat(job))
return job
def get_uploads(self, service_id, limit_days=None, page=1): def get_uploads(self, service_id, limit_days=None, page=1):
params = {"page": page} params = {"page": page}
if limit_days is not None: if limit_days is not None:
params["limit_days"] = limit_days params["limit_days"] = limit_days
return self.get(url="/service/{}/upload".format(service_id), params=params) return self.get(url=f"/service/{service_id}/upload", params=params)
def has_sent_previously( def has_sent_previously(
self, service_id, template_id, template_version, original_file_name self, service_id, template_id, template_version, original_file_name

View File

@@ -8,6 +8,7 @@
{% if current_user.has_permissions() %} {% if current_user.has_permissions() %}
{% if current_user.has_permissions('view_activity') %} {% if current_user.has_permissions('view_activity') %}
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('dashboard') }}" href="{{ url_for('.service_dashboard', service_id=current_service.id) }}">Dashboard</a></li> <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('dashboard') }}" href="{{ url_for('.service_dashboard', service_id=current_service.id) }}">Dashboard</a></li>
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('activity') }}" href="{{ url_for('.all_jobs_activity', service_id=current_service.id) }}">Activity</a></li>
{% endif %} {% endif %}
{% if not current_user.has_permissions('view_activity') %} {% if not current_user.has_permissions('view_activity') %}
<li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li> <li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li>

View File

@@ -22,7 +22,7 @@
{% else %} {% else %}
{% if notifications %} {% if notifications %}
<div class="dashboard-table table-overflow-x-auto"> <div class="dashboard-table job-status-table table-overflow-x-auto">
{% endif %} {% endif %}
{% if job.still_processing %} {% if job.still_processing %}
<p class="bottom-gutter hint"> <p class="bottom-gutter hint">
@@ -40,15 +40,16 @@
notifications, notifications,
caption=uploaded_file_name, caption=uploaded_file_name,
caption_visible=False, caption_visible=False,
border_visible=True,
empty_message='No messages to show yet…' if job.awaiting_processing_or_recently_processed else 'These messages have been deleted because they were sent more than {} days ago'.format(service_data_retention_days), empty_message='No messages to show yet…' if job.awaiting_processing_or_recently_processed else 'These messages have been deleted because they were sent more than {} days ago'.format(service_data_retention_days),
field_headings=[ field_headings=[
'Recipient', 'Recipient',
'Status' 'Message status'
], ],
field_headings_visible=False field_headings_visible=False
) %} ) %}
{% call row_heading() %} {% call row_heading() %}
<a class="usa-link file-list-filename" href="{{ url_for('.view_notification', service_id=current_service.id, notification_id=item.id, from_job=job.id) }}">{{ item.to }}</a> <a class="usa-link file-list-filename" href="{{ url_for('.view_notification', service_id=current_service.id, notification_id=item.id, from_job=job.id) }}">{{ item.to | format_phone_number_human_readable }}</a>
<p class="file-list-hint"> <p class="file-list-hint">
{{ item.preview_of_content }} {{ item.preview_of_content }}
</p> </p>

View File

@@ -0,0 +1,126 @@
{% extends "withnav_template.html" %}
{% block service_page_title %}
All activity
{% endblock %}
{% set show_pagination %}
{% if prev_page or next_page %}
<nav aria-label="Pagination" class="usa-pagination">
<ul class="usa-pagination__list">
{% if prev_page %}
<li class="usa-pagination__item usa-pagination__arrow">
<a
href="{{prev_page['url']}}"
class="usa-pagination__link usa-pagination__previous-page"
aria-label="Previous page"
>
<img src="{{ url_for('static', filename='/img/usa-icons/navigate_before.svg') }}" alt="arrow">
<span class="usa-pagination__link-text">Previous</span></a
>
</li>
{% endif %}
{% if pagination %}
{% for page in pagination.pages %}
{% if page == pagination.current %}
<li class="usa-pagination__item usa-pagination__page-no">
<span class="usa-pagination__button usa-current" aria-label="Page {{ page }}" aria-current="true">
{{ page }}
</span>
</li>
{% else %}
<li class="usa-pagination__item">
<a class="usa-pagination__button" href="?page={{ page }}">
{{ page }}
</a>
</li>
{% endif %}
{% if page == 3 and pagination.last > 4 %}
<li class="usa-pagination__item usa-pagination__overflow" aria-label="ellipsis indicating non-visible pages">
<span></span>
</li>
{% endif %}
{% endfor %}
{% endif %}
{% if next_page %}
<li class="usa-pagination__item usa-pagination__arrow">
<a
href="{{ next_page['url'] }}"
class="usa-pagination__link usa-pagination__next-page"
aria-label="Next page"
>
<span class="usa-pagination__link-text">Next </span>
<img src="{{ url_for('static', filename='/img/usa-icons/navigate_next.svg') }}" alt="arrow">
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endset %}
{% block maincolumn_content %}
<div class="margin-bottom-8">
<h1 class="usa-sr-only">All activity</h1>
<h2 class="font-body-2xl line-height-sans-2 margin-0">All activity</h2>
<h2 class="margin-top-4 margin-bottom-1">Sent jobs</h2>
<div class="usa-table-container--scrollable-mobile">
<table class="usa-table usa-table--compact job-table">
<caption></caption>
<thead class="table-field-headings">
<tr>
<th scope="col" role="columnheader" class="table-field-heading-first" id="jobId">
<span>Job ID#</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<span>Template</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<span>Time sent</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<span>Sender</span>
</th>
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
<span>Report</span>
</th>
</tr>
</thead>
<tbody>
{% if all_jobs_dict %}
{% for job in all_jobs_dict %}
<tr class="table-row">
<td class="table-field jobid" scope="row" role="rowheader">
<a class="usa-link" href="{{ job.view_job_link }}">
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
</a>
</td>
<td class="table-field template">{{ job.template_name }}</td>
<td data-sort-value="{{job.time_sent_data_value}}" class="table-field time-sent">
{{ (job.processing_finished if job.processing_finished else job.processing_started
if job.processing_started else job.created_at)|format_datetime_table }}
</td>
<td class="table-field sender">{{ job.created_by.name }}</td>
<td class="table-field report">
{% if job.time_left != "Data no longer available" %}
<a href="{{ job.download_link }}"><img src="{{ url_for('static', filename='img/material-icons/file_download.svg') }}" alt="File Download Icon"></a>
{% elif job %}
<span>N/A</span>
{% endif %}
</td>
</tr>
{% endfor %}
{% else %}
<tr class="table-row">
<td class="table-empty-message" colspan="10">No batched job messages found (messages are kept for {{ service_data_retention_days }} days).</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="usa-sr-only usa-table__announcement-region" aria-live="polite"></div>
<p><b>Note: </b>Report data is only available for 7 days after your message has been sent</p>
</div>
{{show_pagination}}
</div>
{% endblock %}

View File

@@ -25,8 +25,8 @@
</div> </div>
{% endif %} {% endif %}
<p class="notification-status"> <p class="notification-status max-width-full">
Messages will remain in pending state until carrier status is received, typically 5 minutes. Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear back from the carrier they have received it and attempted deliver. More information on <a class="usa-link" href="{{ url_for('main.message_status') }}">delivery status</a>.
</p> </p>
{% if not job.processing_finished %} {% if not job.processing_finished %}
<div <div

View File

@@ -29,3 +29,15 @@ def generate_previous_next_dict(view, service_id, page, title, url_args):
"title": title, "title": title,
"label": "page {}".format(page), "label": "page {}".format(page),
} }
def generate_pagination_pages(total_items, page_size, current_page):
total_pages = (total_items + page_size - 1) // page_size
pagination = {"current": current_page, "pages": [], "last": total_pages}
if total_pages <= 9:
pagination["pages"] = list(range(1, total_pages + 1))
else:
start_page = max(1, min(current_page - 4, total_pages - 8))
end_page = min(start_page + 8, total_pages)
pagination["pages"] = list(range(start_page, end_page + 1))
return pagination

View File

@@ -27,7 +27,6 @@ plugins.uglify = require('gulp-uglify');
const paths = { const paths = {
src: 'app/assets/', src: 'app/assets/',
dist: 'app/static/', dist: 'app/static/',
templates: 'app/templates/',
npm: 'node_modules/', npm: 'node_modules/',
toolkit: 'node_modules/govuk_frontend_toolkit/', toolkit: 'node_modules/govuk_frontend_toolkit/',
govuk_frontend: 'node_modules/govuk-frontend/' govuk_frontend: 'node_modules/govuk-frontend/'

View File

@@ -2,7 +2,6 @@ from collections import namedtuple
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
import pytz import pytz
from govuk_bank_holidays.bank_holidays import BankHolidays
from notifications_utils.countries.data import Postage from notifications_utils.countries.data import Postage
from notifications_utils.timezones import utc_string_to_aware_gmt_datetime from notifications_utils.timezones import utc_string_to_aware_gmt_datetime
@@ -18,16 +17,6 @@ CANCELLABLE_JOB_LETTER_STATUSES = [
] ]
non_working_days_dvla = BankHolidays(
use_cached_holidays=True,
weekend=(5, 6),
)
non_working_days_royal_mail = BankHolidays(
use_cached_holidays=True,
weekend=(6,), # Only Sunday (day 6 of the week) is a non-working day
)
def set_gmt_hour(day, hour): def set_gmt_hour(day, hour):
return ( return (
day.astimezone(pytz.timezone("Europe/London")) day.astimezone(pytz.timezone("Europe/London"))
@@ -36,28 +25,27 @@ def set_gmt_hour(day, hour):
) )
def get_next_work_day(date, non_working_days): def get_next_work_day(date, non_working_days=None):
next_day = date + timedelta(days=1) next_day = date + timedelta(days=1)
if non_working_days.is_work_day( if non_working_days and non_working_days.is_work_day(
date=next_day.date(), date=next_day.date(),
division=BankHolidays.ENGLAND_AND_WALES,
): ):
return next_day return next_day
return get_next_work_day(next_day, non_working_days) return get_next_work_day(next_day)
def get_next_dvla_working_day(date): def get_next_dvla_working_day(date):
""" """
Printing takes place monday to friday, excluding bank holidays Printing takes place monday to friday, excluding bank holidays
""" """
return get_next_work_day(date, non_working_days=non_working_days_dvla) return get_next_work_day(date)
def get_next_royal_mail_working_day(date): def get_next_royal_mail_working_day(date):
""" """
Royal mail deliver letters on monday to saturday Royal mail deliver letters on monday to saturday
""" """
return get_next_work_day(date, non_working_days=non_working_days_royal_mail) return get_next_work_day(date)
def get_delivery_day(date, *, days_to_deliver): def get_delivery_day(date, *, days_to_deliver):

16
poetry.lock generated
View File

@@ -966,20 +966,6 @@ files = [
{file = "geojson-3.1.0.tar.gz", hash = "sha256:58a7fa40727ea058efc28b0e9ff0099eadf6d0965e04690830208d3ef571adac"}, {file = "geojson-3.1.0.tar.gz", hash = "sha256:58a7fa40727ea058efc28b0e9ff0099eadf6d0965e04690830208d3ef571adac"},
] ]
[[package]]
name = "govuk-bank-holidays"
version = "0.14"
description = "Tool to load UK bank holidays from GOV.UK"
optional = false
python-versions = ">=3.6"
files = [
{file = "govuk-bank-holidays-0.14.tar.gz", hash = "sha256:ce85102423b72908957d25981f616494729686515d5d66c09a1d35a354ce20a6"},
{file = "govuk_bank_holidays-0.14-py3-none-any.whl", hash = "sha256:da485c4a40c6c874c925916e492e3f20b807cffba7eed5f07fb69327aef6b10b"},
]
[package.dependencies]
requests = "*"
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.0.3" version = "3.0.3"
@@ -3106,4 +3092,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12.2" python-versions = "^3.12.2"
content-hash = "6420327e4cabd4b5e6b7903607f5c435c53f98b10b3da42da988a18da5fd1717" content-hash = "b271104f669ce0a8e78fb09299b61cf0502cc81a18213dda00f77c759b6e0209"

View File

@@ -17,7 +17,6 @@ flask-basicauth = "~=0.2"
flask-login = "^0.6" flask-login = "^0.6"
flask-talisman = "*" flask-talisman = "*"
flask-wtf = "^1.2" flask-wtf = "^1.2"
govuk-bank-holidays = "^0.14"
gunicorn = {version = "==22.0.0", extras = ["eventlet"]} gunicorn = {version = "==22.0.0", extras = ["eventlet"]}
humanize = "~=4.10" humanize = "~=4.10"
itsdangerous = "~=2.2" itsdangerous = "~=2.2"

View File

@@ -1362,7 +1362,7 @@ def test_menu_all_services_for_platform_admin_user(
page = str(page) page = str(page)
assert url_for("main.choose_template", service_id=service_one["id"]) in page assert url_for("main.choose_template", service_id=service_one["id"]) in page
assert url_for("main.service_settings", service_id=service_one["id"]) in page assert url_for("main.service_settings", service_id=service_one["id"]) in page
assert url_for('main.api_keys', service_id=service_one['id']) not in page assert url_for("main.api_keys", service_id=service_one["id"]) not in page
def test_route_for_service_permissions( def test_route_for_service_permissions(

View File

@@ -353,10 +353,7 @@ def test_should_show_scheduled_job(
) )
assert page.select("main p a")[0]["href"] == url_for( assert page.select("main p a")[0]["href"] == url_for(
"main.view_template_version", "main.message_status",
service_id=SERVICE_ONE_ID,
template_id="5d729fbd-239c-44ab-b498-75a985f3198f",
version=1,
) )
assert page.select_one("main button[type=submit]").text.strip() == "Cancel sending" assert page.select_one("main button[type=submit]").text.strip() == "Cancel sending"
@@ -421,7 +418,7 @@ def test_should_show_updates_for_one_job_as_json(
assert "failed" in content["counts"] assert "failed" in content["counts"]
assert "Recipient" in content["notifications"] assert "Recipient" in content["notifications"]
assert "2021234567" in content["notifications"] assert "2021234567" in content["notifications"]
assert "Status" in content["notifications"] assert "Message status" in content["notifications"]
assert "Delivered" in content["notifications"] assert "Delivered" in content["notifications"]
assert "01-01-2016 at 12:00 AM" in content["notifications"] assert "01-01-2016 at 12:00 AM" in content["notifications"]
@@ -462,7 +459,7 @@ def test_should_show_updates_for_scheduled_job_as_json(
assert "failed" in content["counts"] assert "failed" in content["counts"]
assert "Recipient" in content["notifications"] assert "Recipient" in content["notifications"]
assert "2021234567" in content["notifications"] assert "2021234567" in content["notifications"]
assert "Status" in content["notifications"] assert "Message status" in content["notifications"]
assert "Delivered" in content["notifications"] assert "Delivered" in content["notifications"]
assert "01-01-2016 at 12:00 AM" in content["notifications"] assert "01-01-2016 at 12:00 AM" in content["notifications"]
@@ -496,5 +493,6 @@ def test_should_show_message_note(
) )
assert normalize_spaces(page.select_one("main p.notification-status").text) == ( assert normalize_spaces(page.select_one("main p.notification-status").text) == (
"Messages will remain in pending state until carrier status is received, typically 5 minutes." 'Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear '
"back from the carrier they have received it and attempted deliver. More information on delivery status."
) )

View File

@@ -0,0 +1,183 @@
from bs4 import BeautifulSoup
from app.utils.pagination import get_page_from_request
from tests.conftest import SERVICE_ONE_ID
MOCK_JOBS = {
"data": [
{
"archived": False,
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"id": "mocked_user_id",
"name": "mocked_user",
},
"id": "55b242b5-9f62-4271-aff7-039e9c320578",
"job_status": "finished",
"notification_count": 1,
"original_file_name": "mocked_file.csv",
"processing_finished": "2024-01-25T23:02:25+00:00",
"processing_started": "2024-01-25T23:02:24+00:00",
"scheduled_for": None,
"service": "21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3",
"service_name": {"name": "Mock Texting Service"},
"statistics": [{"count": 1, "status": "sending"}],
"template": "6a456418-498c-4c86-b0cd-9403c14a216c",
"template_name": "Mock Template Name",
"template_type": "sms",
"template_version": 3,
"updated_at": "2024-01-25T23:02:25+00:00",
}
],
"links": {
"last": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3",
"next": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3",
"prev": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1",
},
"page_size": 50,
"total": 115,
}
def test_all_activity(
client_request,
mocker,
):
current_page = get_page_from_request()
mock_get_page_of_jobs = mocker.patch(
"app.job_api_client.get_page_of_jobs", return_value=MOCK_JOBS
)
response = client_request.get_response(
"main.all_jobs_activity",
service_id=SERVICE_ONE_ID,
page=current_page,
)
assert response.status_code == 200, "Request failed"
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)
page = BeautifulSoup(response.data, "html.parser")
table = page.find("table")
assert table is not None, "Table not found in the response"
headers = [th.get_text(strip=True) for th in table.find_all("th")]
expected_headers = ["Job ID#", "Template", "Time sent", "Sender", "Report"]
assert (
headers == expected_headers
), f"Expected headers {expected_headers}, but got {headers}"
rows = table.find("tbody").find_all("tr", class_="table-row")
assert len(rows) == 1, "Expected one job row in the table"
job_row = rows[0]
cells = job_row.find_all("td")
assert len(cells) == 5, "Expected five columns in the job row"
job_id_cell = cells[0].find("a").get_text(strip=True)
assert (
job_id_cell == "55b242b5"
), f"Expected job ID '55b242b5', but got '{job_id_cell}'"
template_cell = cells[1].get_text(strip=True)
assert (
template_cell == "Mock Template Name"
), f"Expected template 'Mock Template Name', but got '{template_cell}'"
time_sent_cell = cells[2].get_text(strip=True)
assert (
time_sent_cell == "01-25-2024 at 06:02 PM"
), f"Expected time sent '01-25-2024 at 06:02 PM', but got '{time_sent_cell}'"
sender_cell = cells[3].get_text(strip=True)
assert (
sender_cell == "mocked_user"
), f"Expected sender 'mocked_user', but got '{sender_cell}'"
report_cell = cells[4].find("span").get_text(strip=True)
assert report_cell == "N/A", f"Expected report 'N/A', but got '{report_cell}'"
mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page)
def test_all_activity_no_jobs(client_request, mocker):
current_page = get_page_from_request()
mock_get_page_of_jobs = mocker.patch(
"app.job_api_client.get_page_of_jobs",
return_value={
"data": [],
"links": {
"last": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1",
"next": None,
"prev": None,
},
"page_size": 50,
"total": 0,
},
)
response = client_request.get_response(
"main.all_jobs_activity",
service_id=SERVICE_ONE_ID,
page=current_page,
)
assert response.status_code == 200, "Request failed"
page = BeautifulSoup(response.data, "html.parser")
no_jobs_message_td = page.find("td", class_="table-empty-message")
assert no_jobs_message_td is not None, "No jobs message not found in the response"
expected_message = "No batched job messages found (messages are kept for 7 days)."
actual_message = no_jobs_message_td.get_text(strip=True)
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)
def test_all_activity_pagination(client_request, mocker):
current_page = get_page_from_request()
mock_get_page_of_jobs = mocker.patch(
"app.job_api_client.get_page_of_jobs",
return_value={
"data": [
{
"id": f"job-{i}",
"created_at": "2024-01-25T23:02:25+00:00",
"created_by": {"name": "mocked_user"},
"processing_finished": "2024-01-25T23:02:25+00:00",
"processing_started": "2024-01-25T23:02:24+00:00",
"template_name": "Mock Template Name",
"original_file_name": "mocked_file.csv",
"notification_count": 1,
}
for i in range(1, 101)
],
"links": {
"last": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2",
"next": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2",
"prev": None,
},
"page_size": 50,
"total": 100,
},
)
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)
page = BeautifulSoup(response.data, "html.parser")
pagination_controls = page.find_all("li", class_="usa-pagination__item")
assert pagination_controls, "Pagination controls not found in the response"
pagination_texts = [item.get_text(strip=True) for item in pagination_controls]
expected_pagination_texts = ["1", "2", "Next"]
assert (
pagination_texts == expected_pagination_texts
), f"Expected pagination controls {expected_pagination_texts}, but got {pagination_texts}"

View File

@@ -25,6 +25,7 @@ EXCLUDED_ENDPOINTS = tuple(
"add_organization", "add_organization",
"add_service", "add_service",
"add_service_template", "add_service_template",
"all_jobs_activity",
"api_callbacks", "api_callbacks",
"api_documentation", "api_documentation",
"api_integration", "api_integration",
@@ -400,6 +401,7 @@ def test_navigation_urls(
assert [a["href"] for a in page.select(".nav a")] == [ assert [a["href"] for a in page.select(".nav a")] == [
"/services/{}/templates".format(SERVICE_ONE_ID), "/services/{}/templates".format(SERVICE_ONE_ID),
"/services/{}".format(SERVICE_ONE_ID), "/services/{}".format(SERVICE_ONE_ID),
"/activity/services/{}".format(SERVICE_ONE_ID),
# "/services/{}/usage".format(SERVICE_ONE_ID), # "/services/{}/usage".format(SERVICE_ONE_ID),
# "/services/{}/users".format(SERVICE_ONE_ID), # "/services/{}/users".format(SERVICE_ONE_ID),
# "/services/{}/service-settings".format(SERVICE_ONE_ID), # "/services/{}/service-settings".format(SERVICE_ONE_ID),

View File

@@ -1,4 +1,10 @@
from app.utils.pagination import generate_next_dict, generate_previous_dict import pytest
from app.utils.pagination import (
generate_next_dict,
generate_pagination_pages,
generate_previous_dict,
)
def test_generate_previous_dict(client_request): def test_generate_previous_dict(client_request):
@@ -20,3 +26,30 @@ def test_generate_previous_next_dict_adds_other_url_args(client_request):
"main.view_notifications", "foo", 2, {"message_type": "blah"} "main.view_notifications", "foo", 2, {"message_type": "blah"}
) )
assert "notifications/blah" in result["url"] assert "notifications/blah" in result["url"]
@pytest.mark.parametrize(
("total_items", "page_size", "current_page", "expected"),
[
(100, 50, 1, {"current": 1, "pages": [1, 2], "last": 2}),
(450, 50, 1, {"current": 1, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 9}),
(500, 50, 1, {"current": 1, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 10}),
(500, 50, 5, {"current": 5, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 10}),
(500, 50, 6, {"current": 6, "pages": [2, 3, 4, 5, 6, 7, 8, 9, 10], "last": 10}),
(
500,
50,
10,
{"current": 10, "pages": [2, 3, 4, 5, 6, 7, 8, 9, 10], "last": 10},
),
(
950,
50,
15,
{"current": 15, "pages": [11, 12, 13, 14, 15, 16, 17, 18, 19], "last": 19},
),
],
)
def test_generate_pagination_pages(total_items, page_size, current_page, expected):
result = generate_pagination_pages(total_items, page_size, current_page)
assert result == expected