mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-06 03:13:42 -05:00
Merge branch 'main' into 1484-dashboard-visualizations
This commit is contained in:
@@ -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 {
|
||||
ul {
|
||||
list-style: none;
|
||||
@@ -433,27 +457,43 @@ td.table-empty-message {
|
||||
width: 25%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
td.jobid {
|
||||
width: 5%;
|
||||
}
|
||||
td.template {
|
||||
width: 20%;
|
||||
width: 25%;
|
||||
}
|
||||
td.time-sent {
|
||||
width: 20%;
|
||||
width: 30%;
|
||||
}
|
||||
td.sender {
|
||||
width: 15%;
|
||||
width: 20%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
td.count-of-recipients {
|
||||
width: 5%;
|
||||
}
|
||||
td.report {
|
||||
width: 5%;
|
||||
text-align: center;
|
||||
}
|
||||
td.report img {
|
||||
padding-top: 5px;
|
||||
}
|
||||
th {
|
||||
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 {
|
||||
@@ -468,6 +508,10 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
|
||||
.usa-prose > p.max-width-full {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// Tabs
|
||||
|
||||
.tabs {
|
||||
|
||||
@@ -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):
|
||||
# This method assumes that date is in UTC
|
||||
date = parse_naive_dt(date)
|
||||
|
||||
@@ -3,6 +3,7 @@ from flask import Blueprint
|
||||
main = Blueprint("main", __name__)
|
||||
|
||||
from app.main.views import ( # noqa isort:skip
|
||||
activity,
|
||||
add_service,
|
||||
api_keys,
|
||||
choose_account,
|
||||
|
||||
80
app/main/views/activity.py
Normal file
80
app/main/views/activity.py
Normal 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"]
|
||||
]
|
||||
@@ -153,6 +153,9 @@ class HeaderNavigation(Navigation):
|
||||
|
||||
class MainNavigation(Navigation):
|
||||
mapping = {
|
||||
"activity": {
|
||||
"all_jobs_activity",
|
||||
},
|
||||
"dashboard": {
|
||||
"conversation",
|
||||
"inbox",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
{% if current_user.has_permissions() %}
|
||||
{% 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('activity') }}" href="{{ url_for('.all_jobs_activity', service_id=current_service.id) }}">Activity</a></li>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% else %}
|
||||
|
||||
{% if notifications %}
|
||||
<div class="dashboard-table table-overflow-x-auto">
|
||||
<div class="dashboard-table job-status-table table-overflow-x-auto">
|
||||
{% endif %}
|
||||
{% if job.still_processing %}
|
||||
<p class="bottom-gutter hint">
|
||||
@@ -40,15 +40,16 @@
|
||||
notifications,
|
||||
caption=uploaded_file_name,
|
||||
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),
|
||||
field_headings=[
|
||||
'Recipient',
|
||||
'Status'
|
||||
'Message status'
|
||||
],
|
||||
field_headings_visible=False
|
||||
) %}
|
||||
{% 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">
|
||||
{{ item.preview_of_content }}
|
||||
</p>
|
||||
|
||||
126
app/templates/views/activity/all-activity.html
Normal file
126
app/templates/views/activity/all-activity.html
Normal 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 %}
|
||||
@@ -25,8 +25,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="notification-status">
|
||||
Messages will remain in pending state until carrier status is received, typically 5 minutes.
|
||||
<p class="notification-status max-width-full">
|
||||
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>
|
||||
{% if not job.processing_finished %}
|
||||
<div
|
||||
|
||||
@@ -29,3 +29,15 @@ def generate_previous_next_dict(view, service_id, page, title, url_args):
|
||||
"title": title,
|
||||
"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
|
||||
|
||||
@@ -2,7 +2,6 @@ from collections import namedtuple
|
||||
from datetime import datetime, time, timedelta
|
||||
|
||||
import pytz
|
||||
from govuk_bank_holidays.bank_holidays import BankHolidays
|
||||
|
||||
from notifications_utils.countries.data import Postage
|
||||
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):
|
||||
return (
|
||||
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)
|
||||
if non_working_days.is_work_day(
|
||||
if non_working_days and non_working_days.is_work_day(
|
||||
date=next_day.date(),
|
||||
division=BankHolidays.ENGLAND_AND_WALES,
|
||||
):
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
|
||||
16
poetry.lock
generated
16
poetry.lock
generated
@@ -966,20 +966,6 @@ files = [
|
||||
{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]]
|
||||
name = "greenlet"
|
||||
version = "3.0.3"
|
||||
@@ -3106,4 +3092,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12.2"
|
||||
content-hash = "6420327e4cabd4b5e6b7903607f5c435c53f98b10b3da42da988a18da5fd1717"
|
||||
content-hash = "b271104f669ce0a8e78fb09299b61cf0502cc81a18213dda00f77c759b6e0209"
|
||||
|
||||
@@ -17,7 +17,6 @@ flask-basicauth = "~=0.2"
|
||||
flask-login = "^0.6"
|
||||
flask-talisman = "*"
|
||||
flask-wtf = "^1.2"
|
||||
govuk-bank-holidays = "^0.14"
|
||||
gunicorn = {version = "==22.0.0", extras = ["eventlet"]}
|
||||
humanize = "~=4.10"
|
||||
itsdangerous = "~=2.2"
|
||||
|
||||
@@ -353,10 +353,7 @@ def test_should_show_scheduled_job(
|
||||
)
|
||||
|
||||
assert page.select("main p a")[0]["href"] == url_for(
|
||||
"main.view_template_version",
|
||||
service_id=SERVICE_ONE_ID,
|
||||
template_id="5d729fbd-239c-44ab-b498-75a985f3198f",
|
||||
version=1,
|
||||
"main.message_status",
|
||||
)
|
||||
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 "Recipient" 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 "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 "Recipient" 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 "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) == (
|
||||
"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.'
|
||||
)
|
||||
|
||||
179
tests/app/main/views/test_jobs_activity.py
Normal file
179
tests/app/main/views/test_jobs_activity.py
Normal file
@@ -0,0 +1,179 @@
|
||||
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}"
|
||||
)
|
||||
@@ -25,6 +25,7 @@ EXCLUDED_ENDPOINTS = tuple(
|
||||
"add_organization",
|
||||
"add_service",
|
||||
"add_service_template",
|
||||
"all_jobs_activity",
|
||||
"api_callbacks",
|
||||
"api_documentation",
|
||||
"api_integration",
|
||||
@@ -400,6 +401,7 @@ def test_navigation_urls(
|
||||
assert [a["href"] for a in page.select(".nav a")] == [
|
||||
"/services/{}/templates".format(SERVICE_ONE_ID),
|
||||
"/services/{}".format(SERVICE_ONE_ID),
|
||||
"/activity/services/{}".format(SERVICE_ONE_ID),
|
||||
# "/services/{}/usage".format(SERVICE_ONE_ID),
|
||||
# "/services/{}/users".format(SERVICE_ONE_ID),
|
||||
# "/services/{}/service-settings".format(SERVICE_ONE_ID),
|
||||
|
||||
@@ -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):
|
||||
@@ -20,3 +26,20 @@ def test_generate_previous_next_dict_adds_other_url_args(client_request):
|
||||
"main.view_notifications", "foo", 2, {"message_type": "blah"}
|
||||
)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user