merge from main

This commit is contained in:
Kenneth Kehl
2024-09-09 12:09:53 -07:00
25 changed files with 265 additions and 566 deletions

View File

@@ -8,6 +8,7 @@ from notifications_utils import DAILY_MESSAGE_LIMIT
class Config(object):
SIMULATED_SMS_NUMBERS = ("+14254147755", "+14254147167")
NOTIFY_APP_NAME = "admin"
NOTIFY_ENVIRONMENT = getenv("NOTIFY_ENVIRONMENT", "development")
API_HOST_NAME = getenv("API_HOST_NAME", "localhost")

View File

@@ -325,13 +325,19 @@ def aggregate_template_usage(template_statistics, sort_key="count"):
key=lambda x: x["template_id"],
):
template_stats = list(v)
first_stat = template_stats[0] if template_stats else None
templates.append(
{
"template_id": k,
"template_name": template_stats[0]["template_name"],
"template_type": template_stats[0]["template_type"],
"template_name": first_stat.get("template_name"),
"template_type": first_stat.get("template_type"),
"count": sum(s["count"] for s in template_stats),
"created_by": first_stat.get("created_by"),
"created_by_id": first_stat.get("created_by_id"),
"last_used": first_stat.get("last_used"),
"status": first_stat.get("status"),
"template_folder": first_stat.get("template_folder"),
"template_folder_id": first_stat.get("template_folder_id"),
}
)

View File

@@ -5,7 +5,16 @@ from collections import OrderedDict
from datetime import datetime
from io import StringIO
from flask import Response, abort, flash, render_template, request, url_for
from flask import (
Response,
abort,
current_app,
flash,
render_template,
request,
session,
url_for,
)
from notifications_python_client.errors import HTTPError
from app import (
@@ -25,6 +34,7 @@ from app.main.forms import (
DateFilterForm,
RequiredDateFilterForm,
)
from app.main.views.send import _send_notification
from app.statistics_utils import (
get_formatted_percentage,
get_formatted_percentage_two_dp,
@@ -771,3 +781,80 @@ def _get_user_row(r):
row.append(r["password_changed_at"])
row.append(r["state"])
return row
@main.route(
"/platform-admin/load-test",
methods=["POST", "GET"],
)
@user_is_platform_admin
def load_test():
"""
The load test assumes that a service called 'Test service' exists. It will make
the platform admin a member of this service if the platform is not already. All
messagese will be sent in this service.
"""
service = _find_load_test_service()
_prepare_load_test_service(service)
example_template = _find_example_template(service)
# Simulated success
for _ in range(0, 250):
session["recipient"] = current_app.config["SIMULATED_SMS_NUMBERS"][0]
session["placeholders"] = {
"day of week": "Monday",
"color": "blue",
"phone number": current_app.config["SIMULATED_SMS_NUMBERS"][0],
}
_send_notification(service["id"], example_template["id"])
# Simulated failure
for _ in range(0, 250):
session["recipient"] = current_app.config["SIMULATED_SMS_NUMBERS"][1]
session["placeholders"] = {
"day of week": "Wednesday",
"color": "orange",
"phone number": current_app.config["SIMULATED_SMS_NUMBERS"][1],
}
_send_notification(service["id"], example_template["id"])
# For now, just redirect to the splash page so we know it's done
return render_template(
"views/platform-admin/splash-page.html",
)
def _find_example_template(service):
templates = service_api_client.get_service_templates(service["id"])
templates = templates["data"]
for template in templates:
# template = json.loads(template)
if template["name"] == "Example text message template":
return template
raise Exception("Could not find example template for load test")
def _find_load_test_service():
services = service_api_client.find_services_by_name("Test service")
services = services["data"]
for service in services:
if service["name"] == "Test service":
return service
raise Exception("Could not find 'Test service' for load test")
def _prepare_load_test_service(service):
users = user_api_client.get_all_users()
for user in users:
if user["platform_admin"] == "t":
try:
user_api_client.add_user_to_service(
service["id"], user["id"], ["send messages"]
)
except Exception:
current_app.logger.exception(
"Couldnt add user, may already be part of service"
)
pass

View File

@@ -1,3 +1,4 @@
import os
import time
import uuid
from string import ascii_uppercase
@@ -942,8 +943,8 @@ def preview_notification(service_id, template_id):
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
def send_notification(service_id, template_id):
scheduled_for = session.pop("scheduled_for", "")
recipient = get_recipient()
if not recipient:
return redirect(
url_for(
@@ -952,53 +953,10 @@ def send_notification(service_id, template_id):
template_id=template_id,
)
)
upload_id = _send_notification(service_id, template_id)
keys = []
values = []
for k, v in session["placeholders"].items():
keys.append(k)
values.append(v)
data = ",".join(keys)
vals = ",".join(values)
data = f"{data}\r\n{vals}"
filename = (
f"one-off-{uuid.uuid4()}.csv" # {current_user.name} removed from filename
)
my_data = {"filename": filename, "template_id": template_id, "data": data}
upload_id = s3upload(service_id, my_data)
# To debug messages that the user reports have not been sent, we log
# the csv filename and the job id. The user will give us the file name,
# so we can search on that to obtain the job id, which we can use elsewhere
# on the API side to find out what happens to the message.
current_app.logger.info(
hilite(
f"One-off file: {filename} job_id: {upload_id} s3 location: service-{service_id}-notify/{upload_id}.csv"
)
)
form = CsvUploadForm()
form.file.data = my_data
form.file.name = filename
check_message_output = check_messages(service_id, template_id, upload_id, 2)
if "You cannot send to" in check_message_output:
return check_messages(service_id, template_id, upload_id, 2)
job_api_client.create_job(
upload_id,
service_id,
scheduled_for=scheduled_for,
template_id=template_id,
original_file_name=filename,
notification_count=1,
valid="True",
)
session.pop("recipient")
session.pop("placeholders")
session.pop("recipient", "")
session.pop("placeholders", "")
# We have to wait for the job to run and create the notification in the database
time.sleep(0.1)
@@ -1046,6 +1004,56 @@ def send_notification(service_id, template_id):
)
def _send_notification(service_id, template_id):
scheduled_for = session.pop("scheduled_for", "")
keys = []
values = []
for k, v in session["placeholders"].items():
keys.append(k)
values.append(v)
data = ",".join(keys)
vals = ",".join(values)
data = f"{data}\r\n{vals}"
filename = (
f"one-off-{uuid.uuid4()}.csv" # {current_user.name} removed from filename
)
my_data = {"filename": filename, "template_id": template_id, "data": data}
upload_id = s3upload(service_id, my_data)
# To debug messages that the user reports have not been sent, we log
# the csv filename and the job id. The user will give us the file name,
# so we can search on that to obtain the job id, which we can use elsewhere
# on the API side to find out what happens to the message.
current_app.logger.info(
hilite(
f"One-off file: {filename} job_id: {upload_id} s3 location: service-{service_id}-notify/{upload_id}.csv"
)
)
# For load testing we want to skip these checks. They are doing some fine-grained
# comparison about what is in the preview, but the load test just blast messages
# and doesn't care about the preview.
if os.getenv("NOTIFY_ENVIRONMENT") not in ("development", "staging", "demo"):
form = CsvUploadForm()
form.file.data = my_data
form.file.name = filename
check_message_output = check_messages(service_id, template_id, upload_id, 2)
if "You cannot send to" in check_message_output:
return check_messages(service_id, template_id, upload_id, 2)
job_api_client.create_job(
upload_id,
service_id,
scheduled_for=scheduled_for,
template_id=template_id,
original_file_name=filename,
notification_count=1,
valid="True",
)
return upload_id
def get_email_reply_to_address_from_session():
if session.get("sender_id"):
return current_service.get_email_reply_to_address(session["sender_id"])[

View File

@@ -1,3 +1,4 @@
import os
from datetime import datetime
from flask import abort, current_app, request, session
@@ -219,6 +220,14 @@ class User(JSONModel, UserMixin):
def has_permissions(
self, *permissions, restrict_admin_usage=False, allow_org_user=False
):
# TODO need this for load test, but breaks unit tests
if self.platform_admin and os.getenv("NOTIFY_ENVIRONMENT") in (
"development",
"staging",
"demo",
):
return True
unknown_permissions = set(permissions) - all_ui_permissions
if unknown_permissions:
raise TypeError(

View File

@@ -1,15 +0,0 @@
# Footer
## Installation
See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component.
## Guidance and Examples
Find out when to use the footer component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/footer).
## Component options
Use options to customize the appearance, content and behavior of a component when using a macro, for example, changing the text.
See [options table](https://design-system.service.gov.uk/components/footer/#options-example-default) for details.

View File

@@ -1,115 +0,0 @@
[
{
"name": "meta",
"type": "object",
"required": false,
"description": "Object containing options for the meta navigation.",
"params": [
{
"name": "visuallyHiddenTitle",
"type": "string",
"description": "Title for a meta item section, which defaults to Support links"
},
{
"name": "html",
"type": "string",
"description": "HTML to add to the meta section of the footer, which will appear below any links specified using meta.items."
},
{
"name": "text",
"type": "string",
"description": "Text to add to the meta section of the footer, which will appear below any links specified using meta.items. If meta.html is specified, this option is ignored."
},
{
"name": "items",
"type": "array",
"required": false,
"description": "Array of items for use in the meta section of the footer.",
"params": [
{
"name": "text",
"type": "string",
"required": false,
"description": "List item text in the meta section of the footer."
},
{
"name": "href",
"type": "string",
"required": false,
"description": "List item href attribute in the meta section of the footer."
},
{
"name": "attributes",
"type": "object",
"required": false,
"description": "HTML attributes (for example data attributes) to add to the anchor in the footer meta section."
}
]
}
]
},
{
"name": "navigation",
"type": "array",
"required": false,
"description": "Array of items for use in the navigation section of the footer.",
"params": [
{
"name": "title",
"type": "string",
"required": false,
"description": "Title for a section"
},
{
"name": "columns",
"type": "integer",
"required": false,
"description": "Amount of columns to display items in navigation section of the footer."
},
{
"name": "items",
"type": "array",
"required": false,
"description": "Array of items to display in the list in navigation section of the footer.",
"params": [
{
"name": "text",
"type": "string",
"required": false,
"description": "List item text in the navigation section of the footer."
},
{
"name": "href",
"type": "string",
"required": false,
"description": "List item href attribute in the navigation section of the footer. Both `text` and `href` attributes need to be present to create a link."
},
{
"name": "attributes",
"type": "object",
"required": false,
"description": "HTML attributes (for example data attributes) to add to the anchor in the footer navigation section."
}
]
}
]
},
{
"name": "containerClasses",
"type": "string",
"required": false,
"description": "Classes that can be added to the inner container, useful if you want to make the footer full width."
},
{
"name": "classes",
"type": "string",
"required": false,
"description": "Classes to add to the footer component container."
},
{
"name": "attributes",
"type": "object",
"required": false,
"description": "HTML attributes (for example data attributes) to add to the footer component container."
}
]

View File

@@ -1,3 +0,0 @@
{% macro usaFooter(params) %}
{%- include "./template.njk" -%}
{% endmacro %}

View File

@@ -1,85 +0,0 @@
<footer class="usa-footer usa-footer--big">
{# <div class="grid-container usa-footer__return-to-top">
<a href="#">Return to top</a>
</div> #}
<div class="usa-footer__primary-section">
<div class="grid-container">
<div class="padding-y-1">
{% if params.meta %}
<h2 class="usa-sr-only">{{ params.meta.visuallyHiddenTitle | default("Support links") }}</h2>
{% if params.meta.items %}
{% for item in params.meta.items %}
<a class="usa-footer__link" href="{{ item.href }}" {% for attribute, value in item.attributes %}
{{attribute}}="{{value}}" {% endfor %}>
{{ item.text }}
</a>
{% endfor %}
{% endif %}
{% if params.meta.text or params.meta.html %}
<div>
{{ params.meta.html | safe if params.meta.html else params.meta.text }}
</div>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
</footer>
<div class="usa-identifier site-identifier">
<section class="usa-identifier__section usa-identifier__section--masthead" aria-label="Agency identifier,,,,,,">
<div class="usa-identifier__container">
<div class="usa-identifier__logos"><a href="javascript:void(0);" class="usa-identifier__logo">
<img class="usa-identifier__logo-img" src="{{ params.assetsPath | default('/static/images') }}/gsa-logo.svg" alt="GSA Logo" role="img" width="48"
height="48">
</a></div>
<section class="usa-identifier__identity" aria-label="Agency description">
<p class="usa-identifier__identity-domain">beta.notify.gov</p>
<p class="usa-identifier__identity-disclaimer">An official website of the <a href="https://gsa.gov">General
Services Administration</a></p>
</section>
</div>
</section>
<nav class="usa-identifier__section usa-identifier__section--required-links" aria-label="Important links">
<div class="usa-identifier__container">
<ul class="usa-identifier__required-links-list">
<li class="usa-identifier__required-links-item">
<a href="https://www.gsa.gov/about-us" class="usa-identifier__required-link">About GSA</a>
</li>
<li class="usa-identifier__required-links-item">
<a href="https://www.gsa.gov/website-information/accessibility-aids"
class="usa-identifier__required-link">Accessibility support</a>
</li>
<li class="usa-identifier__required-links-item">
<a href="https://www.gsa.gov/reference/freedom-of-information-act-foia"
class="usa-identifier__required-link">FOIA requests</a>
</li>
<li class="usa-identifier__required-links-item">
<a href="https://www.gsa.gov/reference/civil-rights-programs/notification-and-federal-employee-antidiscrimination-and-retaliation-act-of-2002"
class="usa-identifier__required-link">No FEAR Act data</a>
</li>
<li class="usa-identifier__required-links-item">
<a href="https://www.gsaig.gov/" class="usa-identifier__required-link">Office of the Inspector General</a>
</li>
<li class="usa-identifier__required-links-item">
<a href="https://www.gsa.gov/reference/reports/budget-performance"
class="usa-identifier__required-link">Performance reports</a>
</li>
<li class="usa-identifier__required-links-item">
<a href="https://www.gsa.gov/website-information/website-policies"
class="usa-identifier__required-link">Privacy policy</a>
</li>
</ul>
</div>
</nav>
<section class="usa-identifier__section usa-identifier__section--usagov"
aria-label="Government information and services">
<div class="usa-identifier__container">
<div class="usa-identifier__usagov-description">
Looking for U.S. government information and services?
</div>
<a href="https://www.usa.gov/" class="usa-link">Visit USA.gov</a>
</div>
</section>
</div>

View File

@@ -1,15 +0,0 @@
# Header
## Installation
See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component.
## Guidance and Examples
Find out when to use the header component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/header).
## Component options
Use options to customize the appearance, content and behavior of a component when using a macro, for example, changing the text.
See [options table](https://design-system.service.gov.uk/components/header/#options-example-default) for details.

View File

@@ -1,88 +0,0 @@
[
{
"name": "homepageUrl",
"type": "string",
"required": false,
"description": "The url of the homepage. Defaults to /"
},
{
"name": "assetsPath",
"type": "string",
"required": false,
"description": "The public path for the assets folder. If not provided it defaults to /assets/images"
},
{
"name": "productName",
"type": "string",
"required": false,
"description": "Header title that is placed next to GOV.UK. Used for product names (i.e. Pay, Verify)"
},
{
"name": "serviceName",
"type": "string",
"required": false,
"description": "Header title that is placed next to GOV.UK. Used for product names (i.e. Pay, Verify)"
},
{
"name": "serviceUrl",
"type": "string",
"required": false,
"description": "Url for the service name anchor."
},
{
"name": "navigation",
"type": "array",
"required": false,
"description": "An array of navigation item objects.",
"params": [
{
"name": "text",
"type": "string",
"required": false,
"description": "Text of the navigation item."
},
{
"name": "href",
"type": "string",
"required": false,
"description": "Url of the navigation item anchor. Both `href` and `text` attributes for navigation items need to be provided to create an item."
},
{
"name": "active",
"type": "boolean",
"required": false,
"description": "Flag to mark the navigation item as active or not."
},
{
"name": "attributes",
"type": "object",
"required": false,
"description": "HTML attributes (for example data attributes) to add to the navigation item anchor."
}
]
},
{
"name": "navigationClasses",
"type": "string",
"required": false,
"description": "Classes for the navigation section of the header."
},
{
"name": "containerClasses",
"type": "string",
"required": false,
"description": "Classes for the container, useful if you want to make the header fixed width."
},
{
"name": "classes",
"type": "string",
"required": false,
"description": "Classes to add to the header container."
},
{
"name": "attributes",
"type": "object",
"required": false,
"description": "HTML attributes (for example data attributes) to add to the header container."
}
]

View File

@@ -1,3 +0,0 @@
{% macro usaHeader(params) %}
{%- include "./template.njk" -%}
{% endmacro %}

View File

@@ -1,105 +0,0 @@
<!-- usa banner -->
<section class="usa-banner site-banner" aria-label="Official website of the United States government,,,,,,">
<div class="usa-accordion">
<header class="usa-banner__header">
<div class="usa-banner__inner">
<div class="grid-col-auto">
<img aria-hidden="true" class="usa-banner__header-flag" src="/static/img/us_flag_small.png" alt="" />
</div>
<div class="grid-col-fill tablet:grid-col-auto" aria-hidden="true">
<p class="usa-banner__header-text">An official website of the United States government</p>
<p class="usa-banner__header-action">Heres how you know</p>
</div>
<button type="button" class="usa-accordion__button usa-banner__button" aria-expanded="false" aria-controls="gov-banner">
<span class="usa-banner__button-text">Heres how you know</span>
</button>
</div>
</header>
<div class="usa-banner__content usa-accordion__content" id="gov-banner" hidden="">
<div class="grid-row grid-gap-lg">
<div class="usa-banner__guidance tablet:grid-col-6">
<img class="usa-banner__icon usa-media-block__img" src="/static/img/icon-dot-gov.svg" role="img" alt="" aria-hidden="true">
<div class="usa-media-block__body">
<p>
<strong>Official websites use .gov</strong>
<br>
A <strong>.gov</strong> website belongs to an official government organization in the United States.
</p>
</div>
</div>
<div class="usa-banner__guidance tablet:grid-col-6">
<img class="usa-banner__icon usa-media-block__img" src="/static/img/icon-https.svg" role="img" alt="" aria-hidden="true">
<div class="usa-media-block__body">
<p>
<strong>Secure .gov websites use HTTPS</strong>
<br>
A <strong>lock</strong> ( <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" width="52" height="64" viewBox="0 0 52 64" class="usa-banner__lock-image" role="img" aria-labelledby="banner-lock-description" focusable="false"><title id="banner-lock-title">Lock</title><desc id="banner-lock-description">Locked padlock</desc><path fill="#000000" fill-rule="evenodd" d="M26 0c10.493 0 19 8.507 19 19v9h3a4 4 0 0 1 4 4v28a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4h3v-9C7 8.507 15.507 0 26 0zm0 8c-5.979 0-10.843 4.77-10.996 10.712L15 19v9h22v-9c0-6.075-4.925-11-11-11z"></path></svg></span> ) or <strong>https://</strong> means youve safely connected to the .gov website. Share sensitive information only on official, secure websites.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- usa header -->
<header class="usa-header usa-header--extended">
<div class="usa-nav-container">
<div class="usa-navbar">
<div class="usa-logo display-flex flex-align-center flex-justify" id="-logo">
<div class="logo-img display-flex">
<a href="/">
<span class="usa-sr-only">Notify.gov logo</span>
<image src="{{ params.assetsPath | default('/static/images') }}/notify-logo.png" alt="Notify.gov logo" xlink:href=""
class="usa-flag-logo margin-right-1"></image>
</a>
</div>
{% if params.navigation %}
<button type="button" class="usa-menu-btn">Menu</button>
{% endif %}
</div>
</div>
<nav aria-label="Primary navigation" class="usa-nav">
<div class="usa-nav__inner">
<button type="button" class="usa-nav__close">
<img src="/static/images/usa-icons/close.svg" role="img" alt="Close" />
</button>
<ul class="usa-nav__primary usa-accordion margin-right-1">
{% for item in params.navigation %}
{% if item.href and item.text %}
<li class="usa-nav__primary-item{{ ' is-current' if item.active }}">
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
<span>{{ item.text }}</span>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
<div class="usa-nav__secondary margin-bottom-6">
<ul class="usa-nav__secondary-links">
{% for item in params.secondaryNavigation %}
{% if item.href and item.text %}
<li class="usa-nav__secondary-item{{ ' is-current' if item.active }}">
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
<span>{{ item.text }}</span>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
<!-- <section aria-label="Search component">
<form class="usa-search usa-search--small margin-bottom-2" role="search">
<label class="usa-sr-only" for="search-field">Search</label>
<input class="usa-input" id="search-field" type="search" name="search" />
<button class="usa-button" type="submit">
<img src="/static/images/usa-icons-bg/search--white.svg" class="usa-search__submit-icon" alt="Search" />
</button>
</form>
</section> -->
</div>
</div>
</nav>
</div>
</header>

View File

@@ -1,4 +1,3 @@
{# setting navigation and secondarynavigation #}
{% if current_user.is_authenticated %}
{% set navigation = [
{"href": url_for("main.show_accounts_or_dashboard"), "text": "Current service", "active": header_navigation.is_selected('accounts-or-dashboard')},
@@ -30,7 +29,6 @@
{% endif %}
{% endif %}
{# usa header #}
<header class="usa-header usa-header--extended">
<div class="usa-nav-container">
<div class="usa-navbar">

View File

@@ -13,15 +13,6 @@
{% 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>
{% endif %}
{% if current_user.has_permissions('manage_service', allow_org_user=True) %}
{# <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li> #}
{% endif %}
<!-- {% if current_user.has_permissions('manage_api_keys', 'manage_service') %}
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}" href="{{ url_for('.service_settings', service_id=current_service.id) }}">Settings</a></li>
{% endif %} -->
{% if current_user.has_permissions('manage_api_keys') %}
<!-- <li><a class="usa-link{{ main_navigation.is_selected('api-integration') }}" href="{{ url_for('.api_integration', service_id=current_service.id) }}">API integration</a></li> -->
{% endif %}
{% elif current_user.has_permissions(allow_org_user=True) %}
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_users', service_id=current_service.id) }}">Team members</a></li>

View File

@@ -1,5 +1,3 @@
{# usa banner #}
<section class="usa-banner site-banner" aria-label="Official website of the United States government,,,,,,">
<div class="usa-accordion">
<header class="usa-banner__header">

View File

@@ -1,35 +1,45 @@
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading, spark_bar_field %}
<div class="ajax-block-container">
{% if template_statistics|length > 0 %}
<h2 class="margin-top-4 margin-bottom-1">Recent templates</h2>
<div class='template-statistics-table table-overflow-x-auto'>
{% call(item, row_number) list_table(
template_statistics,
caption="Messages sent by template",
caption_visible=False,
border_visible=True,
empty_message='',
field_headings=[
'Template',
'Messages sent'
],
field_headings_visible=False
) %}
{% call row_heading() %}
<a class="usa-link template-statistics-table-template-name" href="{{ url_for('.view_template', service_id=current_service.id, template_id=item.template_id) }}">{{ item.template_name }}</a>
<span class="template-statistics-table-hint">
{{ 1|message_count_label(item.template_type, suffix='template')|capitalize }}
</span>
{% endcall %}
{{ spark_bar_field(item.count, most_used_template_count, id=item.template_id) }}
{% endcall %}
<a
href="{{ url_for('.template_usage', service_id=current_service.id) }}"
class="usa-link show-more-no-border"
><span>See templates used by month</span></a>
{% if template_statistics|length > 1 %}
<h2 class="margin-top-4 margin-bottom-1">Most Used Templates</h2>
<div class="template-statistics-table table-overflow-x-auto">
<table class="usa-table width-full">
<caption class="font-body-lg table-heading usa-sr-only">
Messages sent by template
</caption>
<thead class="table-field-headings">
<tr>
<th class="table-field-heading-first" width="">
<span>Template name</span>
</th>
<th class="table-field-heading" width="">
<span>Folder</span>
</th>
<th class="table-field-heading" width="">
<span>Last used</span>
</th>
<th class="table-field-heading" width="">
<span>Created by</span>
</th>
<th class="table-field-heading" width="">
<span># Times used</span>
</th>
</tr>
</thead>
<tbody>
{% for item in template_statistics[:8] %}
<tr class="table-row">
<td>
<a class="usa-link template-statistics-table-template-name" href="{{ url_for('.view_template', service_id=current_service.id, template_id=item.template_id) }}">{{ item.template_name }}</a>
</td>
<td><p>{{ item.template_folder }}</p></td>
<td><p>{{ item.last_used|format_datetime_table}}</p></td>
<td><p>{{ item.created_by }}</p></td>
<td><p>{{ item.count }}</p></td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="/services/78409625-0c0a-485e-b82c-b19c8f4b1bdb/template-usage" class="usa-link show-more-no-border"><span>See templates by month</span></a>
</div>
{% endif %}
</div>

View File

@@ -28,6 +28,10 @@
{% if not service['active'] %}
<span class="heading-medium hint">&ensp;Archived</span>
{% endif %}
{% if service['name'] == 'Test service' %}
<a class="usa-link" href="{{ url_for('main.load_test') }}">Load Test</a>
{% endif %}
{% endcall %}
{% endcall %}