Compare commits

..

1 Commits

Author SHA1 Message Date
Jonathan Bobel
049ad31dcc removed assignees 2024-01-18 11:23:48 -05:00
25 changed files with 491 additions and 513 deletions

View File

@@ -27,17 +27,6 @@ body:
value: "A bug happened!"
validations:
required: true
- type: dropdown
id: environment
attributes:
label: Environment
description: In which environment did you experience the bug?
options:
- staging
- demo
- production
validations:
required: true
- type: textarea
id: page
attributes:
@@ -45,13 +34,6 @@ body:
description: please include URLs of where you saw the bug
validations:
required: true
- type: textarea
id: reproSteps
attributes:
label: "Detail the steps for someone to reproduce"
description: "Reproduction steps"
validations:
required: true
- type: dropdown
id: browsers
attributes:
@@ -68,4 +50,4 @@ body:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
required: false

View File

@@ -1,82 +0,0 @@
name: "QA Checklist"
description: "Use this template to manually QA for prod push"
title: "QA for prod push: Admin #[insert PR number], API #[insert PR number], Utils #[insert PR number]"
body:
- type: checkboxes
id: Logging_in
attributes:
label: Log In
options:
- label: notify-staging.app.cloud.gov resolves
- label: Clicking sign in takes you to login page
- label: 2FA code is sent promptly
- label: 2FA code input screen appears as expected
- label: After code is entered, you are brought to the “choose service” screen
- label: Choosing a service brings you to the dashboard for that service
- type: checkboxes
id: send_template
attributes:
label: Create a template and send
options:
- label: Clicking “Send messages” brings you to templates page
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
- label: Uploading a csv is quick and automatically directs you to the preview/send page
- label: Formatting looks clean and in place
- label: Message dashboard populates and shows “pending” until call is made to AWS for delivery receipt
- type: checkboxes
id: send__copied_template
attributes:
label: Send a message from copied template
options:
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
- label: Clicking “copy an existing template” allows you to choose from pre-existing template
- label: After editing, the new template saves and you prepare to send
- type: checkboxes
id: send__existing_template
attributes:
label: Send a message from existing template
options:
- label: Clicking on existing template brings you to preview screen with `prepare to send` button
- label: All else above persists as true (formatting, send, dashboard, etc)
- type: checkboxes
id: download_report
attributes:
label: Download the csv delivery report
options:
- label: Clicking on the csv download directly after a message send downloads a report that contains the data we expect
- label: The batch report appears on the main dashboard batch table with accurate information
- label: Clicking on the csv download link from the main dashboard downloads a report that contains the data we expect
- type: checkboxes
id: invite_member
attributes:
label: Invite a team member
options:
- label: Clicking on “Team” in settings brings you to team member screen
- label: Clicking “Invite a team member” brings you to email and permissions screen
- label: Clicking invite team member sends an invite and shows confirmation
- label: Invite email provides live link that works to register new team member
- type: checkboxes
id: delete_member
attributes:
label: Delete a team member
options:
- label: From team member screen, clicking “Change Details” on a specific profile brings you to that persons profile
- label: Clicking on “Remove this team member” prompts a confirmation
- label: Clicking the confirmation removes the team member
- type: checkboxes
id: sign_out
attributes:
label: Sign Out
options:
- label: Hitting “Sign Out” takes you back to the homepage and logs you out

View File

@@ -4,6 +4,3 @@
##
export NEW_RELIC_PROXY_HOST=$egress_proxy
export http_proxy=$egress_proxy
export https_proxy=$egress_proxy
export no_proxy="apps.internal"

View File

@@ -54,7 +54,6 @@ from app.formatters import (
format_datetime_normal,
format_datetime_relative,
format_datetime_short,
format_datetime_short_america,
format_day_of_week,
format_delta,
format_delta_days,
@@ -549,7 +548,6 @@ def add_template_filters(application):
format_datetime_24h,
format_datetime_normal,
format_datetime_short,
format_datetime_short_america,
valid_phone_number,
linkable_name,
format_date,

View File

@@ -124,7 +124,6 @@ td.table-empty-message {
.button-flex-header {
flex-direction: column;
align-items: flex-start;
margin-bottom: units(2);
@include at-media(desktop) {
flex-direction: row;
display: flex;
@@ -142,11 +141,10 @@ td.table-empty-message {
.user-list {
display: inline-flex;
flex-wrap: wrap;
gap: units(4);
gap: units(2);
.user-list-item {
@include at-media(desktop) {
flex: 1 1 calc(50% - units(2));
flex-grow: 0;
}
flex: 1 1 calc(100% - units(2));
border: 1px solid color('gray-cool-10');
@@ -377,32 +375,6 @@ td.table-empty-message {
}
}
.job-table {
.usa-table {
thead th.file-name {
width: 25%;
}
thead th.template {
width: 20%;
}
thead th.time-sent {
width: 30%;
}
thead th.sender {
width: 15%;
}
thead th.\#-of-recipients {
width: 5%;
}
thead th.report {
width: 5%;
}
th {
padding: 0.5rem 0.5rem;
}
}
}
#template-list {
max-height: 500px;
overflow-y: auto;

View File

@@ -98,28 +98,6 @@ def format_datetime_short(date):
)
def format_datetime_short_america(date):
return "{} at {}".format(format_date_numeric_america(date), format_time_12h(date))
def format_date_numeric_america(date):
date = parse_naive_dt(date)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return (
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%m-%d-%Y")
)
def format_time_12h(date):
date = parse_naive_dt(date)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return (
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%I:%M %p")
)
def format_datetime_relative(date):
return "{} at {} {}".format(
get_human_day(date), format_time_24h(date), get_user_preferred_timezone()
@@ -470,12 +448,6 @@ def message_count_noun(count, template_type):
else:
return "text messages"
if template_type == "parts":
if count == 1:
return "text message part"
else:
return "text message parts"
elif template_type == "email":
if count == 1:
return "email"

View File

@@ -47,31 +47,54 @@ def service_dashboard(service_id):
if not current_user.has_permissions("view_activity"):
return redirect(url_for("main.choose_template", service_id=service_id))
download_availability = []
notifications = notification_api_client.get_notifications_for_service(
service_id=service_id,
)["notifications"]
job_response = job_api_client.get_jobs(service_id)
notificaton_job_ids = [
notification["job"]["id"]
for notification in notifications
if "job" in notification
]
jobs = []
for notificaton_job_id in notificaton_job_ids:
job_data = job_api_client.get_job(service_id, notificaton_job_id)["data"]
if job_data:
jobs.append(job_data)
service_data_retention_days = 7
jobs = [
{
"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"]
download_availability = []
for job in jobs:
message_type = job.get("template_type")
if message_type is not None:
service_data_retention_days = current_service.get_days_of_retention(
message_type
)
time_left = get_time_left(job["created_at"])
download_link = (
url_for(
".view_job_csv",
service_id=current_service.id,
job_id=job["id"],
),
"notification_count": job["notification_count"],
"created_by": job["created_by"],
}
for job in job_response["data"]
]
)
download_availability.append(
{
"job_id": job["id"],
"time_left": time_left,
"download_link": download_link,
}
)
return render_template(
"views/dashboard/dashboard.html",
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
partials=get_dashboard_partials(service_id),
notifications=notifications,
jobs=jobs,
download_availability=download_availability,
service_data_retention_days=service_data_retention_days,
)

View File

@@ -11,7 +11,12 @@ from flask import (
url_for,
)
from app import current_service, job_api_client, notification_api_client
from app import (
current_service,
format_date_numeric,
job_api_client,
notification_api_client,
)
from app.main import main
from app.notify_client.api_key_api_client import KEY_TYPE_TEST
from app.utils import (
@@ -21,7 +26,7 @@ from app.utils import (
parse_filter_args,
set_status_filters,
)
from app.utils.csv import generate_notifications_csv, get_user_preferred_timezone
from app.utils.csv import generate_notifications_csv
from app.utils.templates import get_template
from app.utils.user import user_has_permissions
@@ -141,9 +146,6 @@ def download_notifications_csv(service_id):
service_data_retention_days = current_service.get_days_of_retention(
filter_args.get("message_type")[0]
)
file_time = datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
file_time = f"{file_time} {get_user_preferred_timezone()}"
return Response(
stream_with_context(
generate_notifications_csv(
@@ -160,7 +162,7 @@ def download_notifications_csv(service_id):
mimetype="text/csv",
headers={
"Content-Disposition": 'inline; filename="{} - {} - {} report.csv"'.format(
file_time,
format_date_numeric(datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")),
filter_args["message_type"][0],
current_service.name,
)

View File

@@ -780,8 +780,6 @@ def _check_notification(service_id, template_id, exception=None):
back_link = get_back_link(service_id, template, len(placeholders), placeholders)
choose_time_form = ChooseTimeForm()
if (not session.get("recipient")) or not all_placeholders_in_session(
template.placeholders
):
@@ -793,7 +791,6 @@ def _check_notification(service_id, template_id, exception=None):
return dict(
template=template,
back_link=back_link,
choose_time_form=choose_time_form,
**(get_template_error_dict(exception) if exception else {}),
)
@@ -864,7 +861,7 @@ def send_notification(service_id, template_id):
job_api_client.create_job(
upload_id,
service_id,
scheduled_for=request.form.get("scheduled_for", ""),
scheduled_for="",
template_id=template_id,
original_file_name=filename,
notification_count=1,

View File

@@ -29,69 +29,51 @@
{{ ajax_block(partials, updates_url, 'template-statistics') }}
<h2 class="margin-top-4 margin-bottom-1">Recent Batches</h2>
<h2 class="margin-top-4 margin-bottom-1">Batched Jobs</h2>
<div class='job-table'>
<table class="usa-table usa-table--borderless width-full">
<thead class="table-field-headings">
<tr>
<th scope="col" class="table-field-heading-first file-name">
<span>File name</span>
</th>
<th scope="col" class="table-field-heading template">
<span>Template</span>
</th>
<th scope="col" class="table-field-heading time-sent">
<span>Time sent</span>
</th>
<th scope="col" class="table-field-heading sender">
<span>Sender</span>
</th>
<th scope="col" class="table-field-heading #-of-recipients">
<span># of Recipients</span>
</th>
<th scope="col" class="table-field-heading report">
<span>Report</span>
</th>
</tr>
</thead>
<tbody>
{% for notification in notifications[:5] %}
{% if notification %}
<tr class="table-row" id="{{ notification.job.id }}">
<th class="table-field">
{{ notification.job.original_file_name if notification.job.original_file_name else 'Manually entered number'}}
<br>
<a class="usa-link file-list-filename" href="/services/{{ notification.service }}/jobs/{{ notification.job.id }}">View Batch</a>
</th>
<th class="table-field">
{{ notification.template.name }}
</th>
<th class="table-field">
{{ notification.created_at | format_datetime_short_america }}
</th>
<th class="table-field">
{{ notification.created_by.name }}
</th>
{% set job_available = jobs|selectattr('job_id', 'equalto', notification.job.id)|first %}
<th class="table-field">
{{ job_available.notification_count if job_available else ''}}
</th>
<th class="table-field">
{% if job_available and job_available.time_left != "Data no longer available" %}
<a class="usa-link file-list-filename" href="{{ job_available.download_link }}">
{{ "Download" if job_available.job_id else '' }}</a>
<span class="usa-hint">{{ job_available.time_left }}</span>
{% elif job_available %}
<span>{{ job_available.time_left }}</span>
{% endif %}
</th>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% call(item, row_number) list_table(
notifications,
caption="Batched Jobs",
caption_visible=False,
border_visible=True,
empty_message='No batched job messages found &thinsp;(messages are kept for {} days)'.format(service_data_retention_days)|safe,
field_headings=['Template Name','Date/Time', 'Download (CSV) Report'],
field_headings_visible=False
) %}
{% if item.job.original_file_name and item.job.id %}
{% call row_heading() %}
<a class="usa-link file-list-filename" href="/services/{{ item.service }}/jobs/{{ item.job.id }}">{{ item.job.original_file_name|replace('.csv', '') if item.job.id else '' }}</a>
{% endcall %}
{% call row_heading() %}
{{ item.status|format_notification_status_as_time(
item.created_at|format_datetime_short,
(item.updated_at or item.created_at)|format_datetime_short)
}}
{% endcall %}
{% call row_heading() %}
{% set availability = download_availability|selectattr('job_id', 'equalto', item.job.id)|first %}
{% if availability and availability.time_left != "Data no longer available" %}
<a class="usa-link file-list-filename" href="{{ availability.download_link[0] }}">{{ "Download" if item.job.original_file_name else '' }}</a>
<span>- {{ availability.time_left }}</span>
{% elif availability %}
<span>{{ availability.time_left }}</span>
{% endif %}
{% endcall %}
{% endif %}
{% endcall %}
</div>
{% if show_pagination %}
{{ previous_next_navigation(prev_page, next_page) }}
{% elif next_page %}
<p class="table-show-more-link">
Only showing the first 50 messages
</p>
{% endif %}
<h2 class="margin-top-4 margin-bottom-1">Usage</h2>
<h3 class="margin-bottom-0">Daily</h3>
<p class="margin-0">Across all services</p>

View File

@@ -38,9 +38,6 @@
</div>
{% endif %}
<p class="notification-status">
Messages will remain in pending state until carrier status is received, typically 5 minutes.
</p>
{% if not job.processing_finished %}
<div
data-module="update-content"

View File

@@ -54,16 +54,6 @@
)}}" class='page-footer'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
{% if not error %}
{% if choose_time_form %}
{{ choose_time_form.scheduled_for(param_extensions={
'formGroup': {'classes': 'bottom-gutter-2-3'},
'attributes': {
'data-module': 'radio-select',
'data-categories': choose_time_form.scheduled_for.categories|join(','),
'data-show-now-as-default': 'true'
}
}) }}
{% endif %}
{% set button_text %}Send 1 {{ 1|message_count_label(template.template_type, suffix='') }}{% endset %}
{{ usaButton({ "text": button_text }) }}
{% endif %}

View File

@@ -15,20 +15,62 @@ Pricing
{{ content_metadata(
data={
"Last updated": "January 25, 2024"
"Last updated": "Feb 27, 2023"
}
) }}
<p>During the pilot period, each service has an allowance of 250,000 message parts. Once this allowance is met, the
application will stop delivering messages. There's no monthly charge, no setup fee, and no procurement cost. You'll use
more parts towards the allowance if you:</p>
<p>During pilot period, each service has an allowance of 250,000 free text messages. Once this allowance is met, the
application will stop delivering messages. There's no monthly charge, no setup fee and no procurement cost.</p>
<p>When a new service is added, it will start in <a class="usa-link" href="{{ url_for('main.trial_mode_new') }}">trial mode</a>. Moving a service out of trial mode is subject to approval by
the Notify.gov team.</p>
<p>You'll use more messages towards the allowance if you send text messages longer than 160 characters.
</p>
<p>
{% if not current_user.is_authenticated %}
<a class="usa-link" href="{{ url_for('main.register') }}">Create an account,</a> then
add your unique service(s).
</p>
{% else %}
Additional unique services may be added, although moving each service out of trial mode is subject to approval by the
Notify.gov team.
{% endif %}
</p>
<p>When you add a new service it will start in <a class="usa-link"
href="{{ url_for('main.trial_mode_new') }}">trial mode</a>.</p>
<p>Youll use more messages towards the allowance if you send text messages longer than 160 characters.</p>
<!-- <h2 class="font-body-lg" id="emails">Emails</h2>
<p>Its free to send emails through Notify.</p> -->
<h2 class="font-body-lg" id="text-messages">Text messages</h2>
<p>During pilot phase, each service has an annual allowance of 250,000 free text messages. Once this allowance is met,
the application will stop delivering messages.</p>
<!-- <p>When a service has used its annual allowance, it costs {{ sms_rate }} pence (plus VAT) for each text message you send.</p> -->
<p>Youll use more messages towards the allowance, if you send <a class="usa-link"
href="#long-text-messages">text messages longer than 160 characters</a>.</p>
<p>You may also use more messages if you:</p>
<ul class="list list-bullet">
<li>send text messages longer than 160 characters</a></li>
<li>use certain <a class="usa-link" href="#symbols">signs and symbols</a></li>
<li>use <a class="usa-link" href="#accents">accents and accented letters</a></li>
<li>send text messages to <a class="usa-link"
href="#international-numbers">international numbers</a></li>
</ul>
<!-- <p>See <a class="usa-link" href="{{ url_for('main.how_to_pay') }}">how to pay</a>.
<h3 class="font-body-lg" id="free-text-message-allowance">Free text message allowance</h3>
<p>The current allowance is:</p>
<ul class="list list-bullet">
<li>40,000 free text messages for national services</li>
<li>20,000 free text messages for regional services</li>
<li>10,000 free text messages for state-funded schools and GP practices</li>
</ul>
<p>Each unique service you add has a separate allowance.</p> -->
<h3 class="font-body-lg" id="long-text-messages">Long text messages</h3>
<p>If a text message is longer than 160 characters (including spaces), it counts as more than one message.</p>
@@ -40,12 +82,12 @@ more parts towards the allowance if you:</p>
caption_visible=False
) %}
{% for message_length, charge in [
('Up to 160 characters', '1 part'),
('Up to 306 characters', '2 parts'),
('Up to 459 characters', '3 parts'),
('Up to 612 characters', '4 parts'),
('Up to 765 characters', '5 parts'),
('Up to 918 characters', '6 parts'),
('Up to 160 characters', '1 text message'),
('Up to 306 characters', '2 text messages'),
('Up to 459 characters', '3 text messages'),
('Up to 612 characters', '4 text messages'),
('Up to 765 characters', '5 text messages'),
('Up to 918 characters', '6 text messages'),
] %}
{% call row() %}
{{ text_field(message_length) }}
@@ -144,11 +186,11 @@ more parts towards the allowance if you:</p>
caption_visible=False
) %}
{% for message_length, charge in [
('Up to 70 characters', '1 part'),
('Up to 134 characters', '2 parts'),
('Up to 201 characters', '3 parts'),
('Up to 268 characters', '4 parts'),
('Each additional 67 characters', '1 additional part'),
('Up to 70 characters', '1 text message'),
('Up to 134 characters', '2 text messages'),
('Up to 201 characters', '3 text messages'),
('Up to 268 characters', '4 text messages'),
('Each additional 67 characters', '1 additional text message'),
] %}
{% call row() %}
{{ text_field(message_length) }}
@@ -158,9 +200,43 @@ more parts towards the allowance if you:</p>
{% endcall %}
</div>
<h3 class="font-body-lg" id="international-numbers">Trial mode</h3>
<p>When a new service is added, it will start in <a href="/using-notify/trial-mode">trial mode</a>. Moving a service out of trial mode is subject to approval by
the Notify.gov team. Additional unique services may be added, although moving each service out of trial mode is subject
to approval by the Notify.gov team.</p>
<h3 class="font-body-lg" id="international-numbers">Sending text messages to international numbers</h3>
<p>It might cost more to send text messages to international numbers than U.S. ones, depending on the country.</p>
<!-- {% set smsIntRates %}
{{ live_search(target_selector='#international-pricing .table-row', show=True, form=search_form, label='Search by country name or code') }}
{% endblock %}
<div id="international-pricing" class="bottom-gutter-3-2">
{% call mapping_table(
caption='International text message rates',
field_headings=['Country code', 'Country', 'Cost multiplier'],
field_headings_visible=True,
caption_visible=False
) %}
{% for cc, names, billable_units in international_sms_rates %}
{% call row() %}
{{ text_field('+' + cc) }}
{% call field() %}
{% for name in names %}
{{ name }}<br>
{% endfor %}
{% endcall %}
{{ text_field('{}&hairsp;&times;'.format(billable_units)|safe) }}
{% endcall %}
{% endfor %}
{% call row() %}
{{ text_field('+882') }}
{% call field() %}Worldwide{% endcall %}
{{ text_field('Not supported') }}
{% endcall %}
{% endcall %}
</div>
{% endset %}
{{ usaDetails({
"summaryText": "International text message rates",
"id": "sms-pricing-details",
"html": smsIntRates
}) }} -->
{% endblock %}

View File

@@ -7,33 +7,37 @@
{% block content_column_content %}
<h1 class="font-body-2xl margin-bottom-3">Trial mode</h1>
<p>When you add a new service it will start in trial mode. This lets you try out Notify.gov, with a few restrictions.</p>
<h2 class="font-body-lg">Limits while in trial mode</h2>
<p>While your service is in trial mode you can only:</p>
<ul class="list list-bullet">
<li>send 50 text message parts per day</li>
<li>send messages to yourself and other people in your team</li>
</ul>
<p>Each text message is made up of one or more parts.</p>
<ul class="list list-bullet">
<li>Generally, 160 characters is one part (and one text message).</li>
<li>A text message of 160-306 characters is two parts.</li>
</ul>
<p>For more information on how message parts are calculated, see
<a href="/using-notify/pricing">Pricing</a>.</p>
<ul class="list list-bullet">
<li>send 50 text messages per day</li>
<li>send messages to yourself and other people in your team</li>
</ul>
<h2 class="font-body-lg">Before going Live</h2>
<p>Before you request to make your service live so you can send messages to clients:</p>
{% if current_service and current_service.trial_mode %}
<p>
To remove these restrictions, you can <a class="usa-link" href="{{ url_for('.support') }}">request to go live</a>.</p>
{% else %}
<p>
To remove these restrictions:
</p>
<ol class="list list-number">
<li><a class="usa-link" href="{{ url_for('.sign_in') }}">Sign in to Notify</a>.</li>
<li>Go to the <b class="bold">Settings</b> page.</li>
<li>Select <b class="bold">Request to go live</b>.</li>
</ol>
{% endif %}
<p>
When we receive your request well get back to you within one working day.
</p>
<h2 class="font-body-lg" id="before-you-request-to-go-live">Before you request to go live</h2>
<p>You must:</p>
<ul class="list list-bullet">
<li>Add examples of the messages you want to send into Notify.</li>
<li>Update your settings so youre ready to send and receive messages.</li>
<li>Review your Terms of Use in the Memorandum of Understanding.</li>
<li>add examples of the messages you want to send</li>
<li>update your settings so youre ready to send and receive messages</li>
<li>accept our terms of use</li>
</ul>
<h3 class="font-body-lg">Going Live</h3>
<p>To remove the restrictions of Trial Mode and begin sending messages to clients complete the <a href="https://airtable.com/appe4n7jYOALPLcyU/shrIPWnLTw9U1fclL">Live Campaign Form</a>.</p>
<p>We'll get back to you within one working day. </p>
{% endblock %}

View File

@@ -19,25 +19,25 @@
</div>
<div id='pill-selected-item'>
<div class='grid-row'>
<div class='grid-col-12'>
<h2 class="heading-small margin-bottom-1">Daily messages across all services</h2>
<p class="margin-0">You have sent {{ global_message_limit - daily_global_messages_remaining }} of your {{ global_message_limit }} daily messages allowance.</p>
<p class="margin-0"></p>You have {{ daily_global_messages_remaining }} messages remaining.</p>
<h2 class='heading-small margin-bottom-1'>Text message parts</h2>
<h2 class='heading-small margin-bottom-1'>Text messages</h2>
<div class="keyline-block">
You have sent
{{ big_number(sms_sent, 'text message parts of your', smaller=True) }}
{{ big_number(sms_free_allowance, 'free message parts allowance.', smaller=True) }}
{{ big_number(sms_sent, 'messages of your', smaller=True) }}
{{ big_number(sms_free_allowance, 'free messages allowance.', smaller=True) }}
<br />
You have
{% if sms_free_allowance > 0 %}
{{ big_number(sms_allowance_remaining, 'message parts remaining.', smaller=True) }}
{{ big_number(sms_allowance_remaining, 'messages remaining.', smaller=True) }}
{% endif %}
{# {% for row in sms_breakdown %}
{% if row.charged_units > 0 %}
{{ big_number(
row.charged_units,
row.charged_units,
'at {:.2f} pence per message'.format(row.rate * 100),
smaller=True
) }}
@@ -90,7 +90,7 @@
{{ item.month }}
{% endcall %}
{% for counts, template_type in [
(item.sms_counts.0, 'parts'),
(item.sms_counts.0, 'sms'),
] %}
{% call field(align='left') %}
{{ big_number(
@@ -126,7 +126,7 @@
{% endif %}
</ul>
{% endcall %}
{% endcall %}
</div>
{% endif %}
@@ -140,7 +140,7 @@
</div>
<div class="grid-col-8">
<p class="align-with-heading-copy">
What counts as 1 text message part?<br />
What counts as 1 text message?<br />
See <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a>.
</p>
</div>

View File

@@ -170,7 +170,7 @@ def convert_report_date_to_preferred_timezone(db_date_str_in_utc):
utc_date_obj = utc_date_obj.astimezone(pytz.utc)
preferred_timezone = pytz.timezone(get_user_preferred_timezone())
preferred_date_obj = utc_date_obj.astimezone(preferred_timezone)
preferred_tz_created_at = preferred_date_obj.strftime("%Y-%m-%d %I:%M:%S %p")
preferred_tz_created_at = preferred_date_obj.strftime("%Y-%m-%d %H:%M:%S")
return f"{preferred_tz_created_at} {get_user_preferred_timezone()}"

View File

@@ -1,17 +1,6 @@
# Notify Sprint Goals Log
## Sprint: B (1/18/24)
2024 Sprints will be named after mythical creatures
| | Goals | Impact | Milestone(s) |
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------|
| Engineering | Get `staging` stable and bugs fixed in order to do a prod push, finalize Login.gov implementation, complete epic to [remove phone numbers from db](https://github.com/GSA/notifications-api/issues/667), [calculate pre-send message parts](https://github.com/GSA/notifications-admin/issues/1027) to add to confirmation page, finish [E2E testing implementation](https://github.com/GSA/notifications-admin/issues/677), set up [opt-out lists by phone number](https://github.com/GSA/notifications-api/issues/312) | Install required auth, expand message send volume for partners, allow users to see how many parts a send will cost, improve app stability, partition opt-outs so that they aren't across the system | Building Notify.gov |
| UX | [Tackle `Usage and Performance` epic](https://github.com/GSA/notifications-admin/issues/1108) as highest priority group of user stories, break out UX work into more user-stories and correlating epics, prioritize epic/story order, improve [pre-send confirmation page](https://github.com/GSA/notifications-admin/issues/815)| Improve UX | Building Notify.gov|
| Security | Complete necessary control families | Aim to have package completed with enough time to allow for long assessment| Secure ATO |
| Content | Get [refreshed content for `Using Notify` pages](https://github.com/GSA/notifications-admin/issues/449) in place, make content [more clear about limits](https://github.com/GSA/notifications-admin/issues/963), make content clearer by [adding `parts` where necessary](https://github.com/GSA/notifications-admin/issues/963) | Improve clarity of message parts for users, bring existing content into current state| Building Notify.gov |
| Ops | Assist in Tech to Gov process, plan gap coverage for Tim's departure 2/9 |
## Sprint: Akhlut (1/3/24)
## Sprint: A (1/3/24)
2024 Sprints will be named after mythical creatures
| | Goals | Impact | Milestone(s) |

125
poetry.lock generated
View File

@@ -41,24 +41,24 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
[[package]]
name = "bandit"
version = "1.7.7"
version = "1.7.6"
description = "Security oriented static analyser for python code."
optional = false
python-versions = ">=3.8"
files = [
{file = "bandit-1.7.7-py3-none-any.whl", hash = "sha256:17e60786a7ea3c9ec84569fd5aee09936d116cb0cb43151023258340dbffb7ed"},
{file = "bandit-1.7.7.tar.gz", hash = "sha256:527906bec6088cb499aae31bc962864b4e77569e9d529ee51df3a93b4b8ab28a"},
{file = "bandit-1.7.6-py3-none-any.whl", hash = "sha256:36da17c67fc87579a5d20c323c8d0b1643a890a2b93f00b3d1229966624694ff"},
{file = "bandit-1.7.6.tar.gz", hash = "sha256:72ce7bc9741374d96fb2f1c9a8960829885f1243ffde743de70a19cee353e8f3"},
]
[package.dependencies]
colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
GitPython = ">=3.1.30"
PyYAML = ">=5.3.1"
rich = "*"
stevedore = ">=1.20.0"
[package.extras]
baseline = ["GitPython (>=3.1.30)"]
test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"]
test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "tomli (>=1.1.0)"]
toml = ["tomli (>=1.1.0)"]
yaml = ["PyYAML"]
@@ -915,6 +915,37 @@ files = [
{file = "geojson-3.1.0.tar.gz", hash = "sha256:58a7fa40727ea058efc28b0e9ff0099eadf6d0965e04690830208d3ef571adac"},
]
[[package]]
name = "gitdb"
version = "4.0.11"
description = "Git Object Database"
optional = false
python-versions = ">=3.7"
files = [
{file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"},
{file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"},
]
[package.dependencies]
smmap = ">=3.0.1,<6"
[[package]]
name = "gitpython"
version = "3.1.41"
description = "GitPython is a Python library used to interact with Git repositories"
optional = false
python-versions = ">=3.7"
files = [
{file = "GitPython-3.1.41-py3-none-any.whl", hash = "sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c"},
{file = "GitPython-3.1.41.tar.gz", hash = "sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048"},
]
[package.dependencies]
gitdb = ">=4.0.1,<5"
[package.extras]
test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "sumtypes"]
[[package]]
name = "govuk-bank-holidays"
version = "0.13"
@@ -1630,40 +1661,40 @@ files = [
[[package]]
name = "newrelic"
version = "9.6.0"
version = "9.5.0"
description = "New Relic Python Agent"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
{file = "newrelic-9.6.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:09dad0db993402e166e37d99302c2ad5588b4ff1e5b814819540ca5ec2bd3cea"},
{file = "newrelic-9.6.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:aeadc0bd8140738af4d7d7ea9d0a3dfa7b6f37e02fe48c0a035ccb6cac881770"},
{file = "newrelic-9.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:16fb3bfde6dca0edab358c2b083386e2e764c463d85ffeb41cd6ce7a279bd7f6"},
{file = "newrelic-9.6.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1ba2fc6ce0b6b9a399a1e5db7e480ba51b431cb3cc6ba136a4c0e76877cf972d"},
{file = "newrelic-9.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fe927f79db2dd5432a362aedde98543e93599b26d5c2e065bc563991be7f347"},
{file = "newrelic-9.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fefffff32e6605ddf26d51030ea1a41b9f7f606fccbb5f8d4b43c36d755acd1"},
{file = "newrelic-9.6.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0a68dd85a7cf4c5039ea94c8975f86a7c516dd492d8bf08976ab0a459918da9b"},
{file = "newrelic-9.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:74a9a49ef596aa9f7560f6bb1480480b8f9b652c7e03de173ef06b0dc2355799"},
{file = "newrelic-9.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26255aec6b29bc17e5e089bb18fbf004f4c765386ba61d1289681aa7ba538207"},
{file = "newrelic-9.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6f21f7a69294f64daa9be59c1c1e0eba889e85ed83afc67ed0a63c8efc9ff2b"},
{file = "newrelic-9.6.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5ecd44ff5b39138469778b5224a1a373867d21a0ca2389d093d99939cb422947"},
{file = "newrelic-9.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c6c4aa8b91dcb5403862c3e5efa57751a8b4e778576916bb22cafbfc03d47807"},
{file = "newrelic-9.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f02fb9166533f22fdb465f7a8d4b364178d94561b159cc9922f4d27491fd7d1"},
{file = "newrelic-9.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703ee92dda983eb4dcbcc300750a815ff3a5401995c3cdd587ac785b6d747e0e"},
{file = "newrelic-9.6.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ce4646554246c6577f5aea358516ad5cf2c35fcef0cd16dbd01bbe89b19b4ccb"},
{file = "newrelic-9.6.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5fae1f3addb422629c4d3e9d011de40629b4d078073ad0517ad42d4ddc7e8441"},
{file = "newrelic-9.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333bd5aedb4f814447ac3aa1a0cc76910a64f13385aa6481a97f87585af52cbf"},
{file = "newrelic-9.6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a5aa22ce7813294ca5dc89a0d86ce4774bb9201028305975efdcf93ef36e0e"},
{file = "newrelic-9.6.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:01c0eb630bb18261241a37aa0a70cb6f706079a1f58f59f2bb64f26fda54ffc5"},
{file = "newrelic-9.6.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:eb82f31754dadd27676fe6f681b25db7af75eb854e332b92f1ea17584efc322c"},
{file = "newrelic-9.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38256e67b2d295229240c29e9949cdae40c9ac987d51f569f57f52ed2aef027f"},
{file = "newrelic-9.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f681c005b1d44ec01b3fe1a7b4ec589572f681f27130ea367155ac0245e133d4"},
{file = "newrelic-9.6.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:23d54298bc0a936a17e25fdca52c2eefbc50966985738e31d62cfe12ea3f60ba"},
{file = "newrelic-9.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f346aeb418c9da6cff4382cb4f50d314ae5126a6a6037f01224b0072eb30ff49"},
{file = "newrelic-9.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26560dba7cd36c5c787e3dfd6c3c5d1e8e8605c82dc7ffee143880456d8650e5"},
{file = "newrelic-9.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d168349041922f206b72fa30a930c0752d8b76b4a2fd7774d4cb2843aa804e32"},
{file = "newrelic-9.6.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:72b378b4aa4a52d2ac14be962f0de747f573b9912365981c0b3fc16bd12b5133"},
{file = "newrelic-9.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ab342ab7e3189e244e44238aa8563eeff3a5b5896b77d2d4dbf2c5d0bfe7d73"},
{file = "newrelic-9.6.0.tar.gz", hash = "sha256:3db642583cfee951f185696fe365b41d8aff733b23867e88de97de114e3cfcf8"},
{file = "newrelic-9.5.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:87c5ea1db9b2f573c7fb0961b7a856a91db0c9f130303de1839c542586b8dc87"},
{file = "newrelic-9.5.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8c62c07a69b86c54d3b9ce187551f36683ad8b70b6301273e7c1e89b121b8245"},
{file = "newrelic-9.5.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:847dd07961adec25c4c1128e8666f72ba0ab03640a44a65bcf25ffebd6fc2a75"},
{file = "newrelic-9.5.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:12be024a5dfd1667d05d7cce968189e2490b6f6d24f194695365607b2340be20"},
{file = "newrelic-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c381d300d904e2a5eb60953a29a729dbd4d8213f6dd629cecaa3033db70a9060"},
{file = "newrelic-9.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2e4ccd5fe97496806fe0ea1acb8b15791c1f012aa44fb047fc8016bcfb80ab9"},
{file = "newrelic-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:14ef5e94d490315a73cf4bea4cd0186b2ec68009e90ab2c16a0b501942fa8629"},
{file = "newrelic-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:306e54751fe1eba61884b0ed63e9583711b37e24db9c7b36725317c21aa609f5"},
{file = "newrelic-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ac2b3603cc781ef58e51a95b0b58220feb3e322c282c758c3bf5430398a47bc"},
{file = "newrelic-9.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5b0a3fbbd6ef1dca0d2434c8ddd15d945fef2cd343d09b3a352b8de825d2e83"},
{file = "newrelic-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab045fe37dcfd9c1fefc84f72373c54abd546d7517909fabd6920ea1d87c3f7f"},
{file = "newrelic-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e54492c5f70cf0951ffc97ddd71b3cf64d68b4d670012d7a7d5ff3dc19417dab"},
{file = "newrelic-9.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92968b7c6951f3ee0fe85a465ae79d448e587a3d10bf71d85b2cab09aa319776"},
{file = "newrelic-9.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847d75a08ca3ba356b6d6317618243e546dc7725251a453d4730ac6e9f043b71"},
{file = "newrelic-9.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec0fe958c5ed52cd8b496339df8661badbd24bdc290dea8744de94da705ccef0"},
{file = "newrelic-9.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a2af10be7893f60588e2103b2f5ff616baf8132ecce90d9a8c7bfb832da08748"},
{file = "newrelic-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2556ae2b9c13127dc46a6326c1ea682eae2c2b64b6873cfd4d5e177c7e0ce58a"},
{file = "newrelic-9.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e9818646a7a72332e5ab3853277187af5b0d51312c51585a5581453fbdc1508"},
{file = "newrelic-9.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b370179f644c0265e4467221e895dccc75440388c5cce8d6a89d150c6b265021"},
{file = "newrelic-9.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c514f82942f7759bc4a360d908c4be37b8764a3ff1589ba3ad8b4baf201bad36"},
{file = "newrelic-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15d4c2dd584c7604a03b2c73464ed3054cd8f0a513ed25832f1a3af4eb6e80ab"},
{file = "newrelic-9.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2af79b43e3244ce7775eb7a0bf2e8fe2cb1624f6eb1be03cb6c35f9e8a5be77"},
{file = "newrelic-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1dab3ef8baffebcf3f088a4131d93f3ac0e1fc08fa6045927c6bd2746b4decf4"},
{file = "newrelic-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78ca19f47b2bc38e18516c2a7c485eeac6979b271a1a5a4ac3b950581ca8a230"},
{file = "newrelic-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63314277c983cd412b87dfa3ed101b88c40ccab683a98046d76bf44a33cb7637"},
{file = "newrelic-9.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68917968adba4abde385be1c23d8fdb13714968883c3a3d1a83fb676ea614245"},
{file = "newrelic-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fb49e3a2e8ab9b509c8d276382f7ce0f6b4e51758b0edec38131ae583532b121"},
{file = "newrelic-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4638e6c28ebf2b5a8a860ae33105a1816425f6ece0113df626c88e5626b22f41"},
{file = "newrelic-9.5.0.tar.gz", hash = "sha256:2442449c4798989d92f527a82e60c446821c97896d739492b607bf6d4018ba1b"},
]
[package.extras]
@@ -1700,7 +1731,7 @@ requests = ">=2.0.0"
[[package]]
name = "notifications-utils"
version = "0.2.7"
version = "0.2.6"
description = ""
optional = false
python-versions = ">=3.9,<3.12"
@@ -1752,7 +1783,7 @@ werkzeug = "^3.0.1"
type = "git"
url = "https://github.com/GSA/notifications-utils.git"
reference = "HEAD"
resolved_reference = "b6cee72f45dbcd48b59447fa08bbac59e15a7b98"
resolved_reference = "f183d120a8b86e655405694adde1f0d95e4e5a51"
[[package]]
name = "numpy"
@@ -2400,13 +2431,13 @@ six = ">=1.5"
[[package]]
name = "python-dotenv"
version = "1.0.1"
version = "1.0.0"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
{file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"},
{file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
]
[package.extras]
@@ -2476,7 +2507,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@@ -2846,6 +2876,17 @@ files = [
{file = "smartypants-2.0.1-py2.py3-none-any.whl", hash = "sha256:8db97f7cbdf08d15b158a86037cd9e116b4cf37703d24e0419a0d64ca5808f0d"},
]
[[package]]
name = "smmap"
version = "5.0.1"
description = "A pure Python implementation of a sliding window memory map manager"
optional = false
python-versions = ">=3.7"
files = [
{file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"},
{file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"},
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
@@ -3088,4 +3129,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<3.12"
content-hash = "94e6fe2e143f12eaa31f01b5a9206012a6a48908d98daa9fa2f531e986e4a6d3"
content-hash = "4794f0a44c97a4e7b2b49341f56b826d10ac6652dfdbc29323f7e433e10dc65d"

View File

@@ -32,7 +32,7 @@ pyexcel-xls = "==0.7.0"
pyexcel-xlsx = "==0.6.0"
openpyxl = "==3.0.10"
pyproj = "==3.6.1"
python-dotenv = "==1.0.1"
python-dotenv = "==1.0.0"
pytz = "==2023.3.post1"
rtreelib = "==0.2.0"
werkzeug = "^3.0.1"

View File

@@ -71,32 +71,49 @@ FAKE_ONE_OFF_NOTIFICATION = {
"total": 1,
}
MOCK_JOBS = {
"data": [
{
"archived": False,
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"id": "mocked_user_id",
"name": "mocked_user",
},
"id": "mocked_notification_id",
"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",
MOCK_ONE_OFF_JOB = {
"data": {
"api_key": "mocked_api_key",
"billable_units": 1,
"carrier": "mocked_carrier",
"client_reference": "mocked_client_reference",
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"email_address": "mocked_email@example.com",
"id": "mocked_user_id",
"name": "mocked_user",
},
"document_download_count": None,
"id": "mocked_notification_id",
"international": False,
"job": {"id": "mocked_job_id", "original_file_name": "mocked_file.txt"},
"job_row_number": 0,
"key_name": "mocked_key_name",
"key_type": "normal",
"normalised_to": "+12133166548",
"notification_type": "sms",
"personalisation": {"phonenumber": "+12133166548"},
"phone_prefix": "1",
"provider_response": "mocked_provider_response",
"rate_multiplier": 1.0,
"reference": "mocked_reference",
"reply_to_text": "mocked_reply_text",
"sent_at": "2024-01-04T20:43:53+00:00",
"sent_by": "mocked_sender",
"service": "mocked_service_id",
"status": "sending",
"template": {
"content": "((day of week)) and ((fave color))",
"id": "bd9caa7e-00ee-4c5a-839e-10ae1a7e6f73",
"name": "personalized",
"redact_personalisation": False,
"subject": None,
"template_type": "sms",
"template_version": 3,
"updated_at": "2024-01-25T23:02:25+00:00",
}
]
"version": 1,
},
"to": "+12133166548",
"updated_at": "2024-01-04T20:43:53+00:00",
}
}
@@ -292,7 +309,7 @@ def test_accepting_invite_removes_invite_from_session(
sample_invite["email_address"] = user["email_address"]
client_request.login(user)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -708,7 +725,7 @@ def test_new_invited_user_verifies_and_added_to_service(
token="thisisnotarealtoken",
_expected_redirect=url_for("main.register_from_invite"),
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,

View File

@@ -85,32 +85,49 @@ FAKE_ONE_OFF_NOTIFICATION = {
"total": 1,
}
MOCK_JOBS = {
"data": [
{
"archived": False,
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"id": "mocked_user_id",
"name": "mocked_user",
},
"id": "mocked_notification_id",
"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",
MOCK_ONE_OFF_JOB = {
"data": {
"api_key": "mocked_api_key",
"billable_units": 1,
"carrier": "mocked_carrier",
"client_reference": "mocked_client_reference",
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"email_address": "mocked_email@example.com",
"id": "mocked_user_id",
"name": "mocked_user",
},
"document_download_count": None,
"id": "mocked_notification_id",
"international": False,
"job": {"id": "mocked_job_id", "original_file_name": "mocked_file.txt"},
"job_row_number": 0,
"key_name": "mocked_key_name",
"key_type": "normal",
"normalised_to": "+12133166548",
"notification_type": "sms",
"personalisation": {"phonenumber": "+12133166548"},
"phone_prefix": "1",
"provider_response": "mocked_provider_response",
"rate_multiplier": 1.0,
"reference": "mocked_reference",
"reply_to_text": "mocked_reply_text",
"sent_at": "2024-01-04T20:43:53+00:00",
"sent_by": "mocked_sender",
"service": "mocked_service_id",
"status": "sending",
"template": {
"content": "((day of week)) and ((fave color))",
"id": "bd9caa7e-00ee-4c5a-839e-10ae1a7e6f73",
"name": "personalized",
"redact_personalisation": False,
"subject": None,
"template_type": "sms",
"template_version": 3,
"updated_at": "2024-01-25T23:02:25+00:00",
}
]
"version": 1,
},
"to": "+12133166548",
"updated_at": "2024-01-04T20:43:53+00:00",
}
}
stub_template_stats = [
@@ -201,7 +218,7 @@ def test_get_started(
"app.template_statistics_client.get_template_statistics_for_service",
return_value=copy.deepcopy(stub_template_stats),
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -231,7 +248,7 @@ def test_get_started_is_hidden_once_templates_exist(
"app.template_statistics_client.get_template_statistics_for_service",
return_value=copy.deepcopy(stub_template_stats),
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -258,7 +275,7 @@ def test_inbound_messages_not_visible_to_service_without_permissions(
mock_get_inbound_sms_summary,
):
service_one["permissions"] = []
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -286,6 +303,7 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_messages(
mock_get_inbound_sms_summary,
):
service_one["permissions"] = ["inbound_sms"]
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -294,8 +312,7 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_messages(
"main.service_dashboard",
service_id=SERVICE_ONE_ID,
)
mock_get_jobs.assert_called_with(SERVICE_ONE_ID)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID)
banner = page.select("a.banner-dashboard")[1]
assert (
normalize_spaces(banner.text)
@@ -318,6 +335,7 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
mock_get_inbound_sms_summary_with_no_messages,
):
service_one["permissions"] = ["inbound_sms"]
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -326,8 +344,7 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
"main.service_dashboard",
service_id=SERVICE_ONE_ID,
)
mock_get_jobs.assert_called_with(SERVICE_ONE_ID)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID)
banner = page.select("a.banner-dashboard")[1]
assert normalize_spaces(banner.text) == "0 text messages received"
assert banner["href"] == url_for("main.inbox", service_id=SERVICE_ONE_ID)
@@ -568,7 +585,7 @@ def test_should_show_recent_templates_on_dashboard(
"app.template_statistics_client.get_template_statistics_for_service",
return_value=copy.deepcopy(stub_template_stats),
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -625,7 +642,7 @@ def test_should_not_show_recent_templates_on_dashboard_if_only_one_template_used
"app.template_statistics_client.get_template_statistics_for_service",
return_value=stats,
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -765,6 +782,7 @@ def test_should_show_upcoming_jobs_on_dashboard(
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary,
):
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -774,8 +792,7 @@ def test_should_show_upcoming_jobs_on_dashboard(
service_id=SERVICE_ONE_ID,
)
mock_get_jobs.assert_called_with(SERVICE_ONE_ID)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID)
mock_get_scheduled_job_stats.assert_called_once_with(SERVICE_ONE_ID)
assert normalize_spaces(page.select_one("main h2").text) == ("In the next few days")
@@ -807,7 +824,7 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_count_is_0(
"soonest_scheduled_for": None,
},
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -833,7 +850,7 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_service_has_no_jobs(
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary,
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -887,7 +904,7 @@ def test_correct_font_size_for_big_numbers(
service_one["permissions"] = permissions
mocker.patch("app.main.views.dashboard.get_dashboard_totals", return_value=totals)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -921,6 +938,7 @@ def test_should_not_show_jobs_on_dashboard_for_users_with_uploads_page(
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary,
):
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -929,8 +947,7 @@ def test_should_not_show_jobs_on_dashboard_for_users_with_uploads_page(
"main.service_dashboard",
service_id=SERVICE_ONE_ID,
)
mock_get_jobs.assert_called_with(SERVICE_ONE_ID)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID)
for filename in {
"export 1/1/2016.xls",
"all email addresses.xlsx",
@@ -975,8 +992,8 @@ def test_usage_page(
sms_column = normalize_spaces(annual_usage[0].text)
assert (
"You have sent 251,800 text message parts of your 250,000 free message parts allowance."
" You have 0 message parts remaining." in sms_column
"You have sent 251,800 messages of your 250,000 free messages allowance. You have 0 messages remaining."
in sms_column
)
assert "$29.85 spent" not in sms_column
assert "1,500 at 1.65 pence" not in sms_column
@@ -1012,8 +1029,8 @@ def test_usage_page_no_sms_spend(
annual_usage = page.find_all("div", {"class": "keyline-block"})
sms_column = normalize_spaces(annual_usage[0].text)
assert (
"You have sent 1,000 text message parts of your 250,000 free message parts allowance."
" You have 249,000 message parts remaining." in sms_column
"You have sent 1,000 messages of your 250,000 free messages allowance. You have 249,000 messages remaining."
in sms_column
)
assert "$0.00 spent" not in sms_column
assert "pence per message" not in sms_column
@@ -1089,7 +1106,7 @@ def test_usage_page_with_0_free_allowance(
sms_column = normalize_spaces(annual_usage[0].text)
assert (
"You have sent 251,800 text message parts of your 0 free message parts allowance. You have"
"You have sent 251,800 messages of your 0 free messages allowance. You have"
in sms_column
)
assert "free allowance remaining" not in sms_column
@@ -1192,7 +1209,7 @@ def test_menu_send_messages(
):
service_one["permissions"] = ["email", "sms"]
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1229,7 +1246,7 @@ def test_menu_manage_service(
mock_get_inbound_sms_summary,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1266,7 +1283,7 @@ def test_menu_main_settings(
mock_get_inbound_sms_summary,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1302,7 +1319,7 @@ def test_menu_manage_api_keys(
mock_get_inbound_sms_summary,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1342,7 +1359,7 @@ def test_menu_all_services_for_platform_admin_user(
mock_get_inbound_sms_summary,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1387,7 +1404,7 @@ def test_route_for_service_permissions(
mocker.patch(
"app.service_api_client.get_global_notification_count", side_effect=_get
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1443,7 +1460,7 @@ def test_service_dashboard_updates_gets_dashboard_totals(
"sms": {"requested": 456, "delivered": 0, "failed": 0},
},
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1569,7 +1586,7 @@ def test_org_breadcrumbs_do_not_show_if_service_has_no_org(
mock_get_annual_usage_for_service,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1636,7 +1653,7 @@ def test_org_breadcrumbs_show_if_user_is_a_member_of_the_services_org(
id_=ORGANISATION_ID,
),
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1672,7 +1689,7 @@ def test_org_breadcrumbs_do_not_show_if_user_is_a_member_of_the_services_org_but
)
mocker.patch("app.models.service.Organization")
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
@@ -1710,7 +1727,7 @@ def test_org_breadcrumbs_show_if_user_is_platform_admin(
),
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
@@ -1746,7 +1763,7 @@ def test_breadcrumb_shows_if_service_is_suspended(
"app.service_api_client.get_service", return_value={"data": service_one_json}
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
@@ -1780,7 +1797,7 @@ def test_service_dashboard_shows_usage(
"count": 500,
},
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1823,7 +1840,7 @@ def test_service_dashboard_shows_free_allowance(
}
],
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
@@ -1847,7 +1864,7 @@ def test_service_dashboard_shows_batched_jobs(
mock_get_annual_usage_for_service,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,

View File

@@ -349,10 +349,9 @@ def test_should_show_scheduled_job(
job_id=fake_uuid,
)
assert normalize_spaces(page.select("main div p")[1].text) == (
assert normalize_spaces(page.select("main p")[1].text) == (
"Sending Two week reminder today at 00:00 US/Eastern"
)
assert page.select("main p a")[0]["href"] == url_for(
"main.view_template_version",
service_id=SERVICE_ONE_ID,
@@ -482,22 +481,3 @@ def test_should_show_updates_for_scheduled_job_as_json(
@freeze_time("2016-01-10 12:00:00.000000")
def test_time_left(job_created_at, expected_message):
assert get_time_left(job_created_at) == expected_message
def test_should_show_message_note(
client_request,
mock_get_service_template,
mock_get_scheduled_job,
mock_get_service_data_retention,
mock_get_notifications,
fake_uuid,
):
page = client_request.get(
"main.view_job",
service_id=SERVICE_ONE_ID,
job_id=fake_uuid,
)
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."
)

View File

@@ -57,32 +57,49 @@ FAKE_ONE_OFF_NOTIFICATION = {
"total": 1,
}
MOCK_JOBS = {
"data": [
{
"archived": False,
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"id": "mocked_user_id",
"name": "mocked_user",
},
"id": "mocked_notification_id",
"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",
MOCK_ONE_OFF_JOB = {
"data": {
"api_key": "mocked_api_key",
"billable_units": 1,
"carrier": "mocked_carrier",
"client_reference": "mocked_client_reference",
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"email_address": "mocked_email@example.com",
"id": "mocked_user_id",
"name": "mocked_user",
},
"document_download_count": None,
"id": "mocked_notification_id",
"international": False,
"job": {"id": "mocked_job_id", "original_file_name": "mocked_file.txt"},
"job_row_number": 0,
"key_name": "mocked_key_name",
"key_type": "normal",
"normalised_to": "+12133166548",
"notification_type": "sms",
"personalisation": {"phonenumber": "+12133166548"},
"phone_prefix": "1",
"provider_response": "mocked_provider_response",
"rate_multiplier": 1.0,
"reference": "mocked_reference",
"reply_to_text": "mocked_reply_text",
"sent_at": "2024-01-04T20:43:53+00:00",
"sent_by": "mocked_sender",
"service": "mocked_service_id",
"status": "sending",
"template": {
"content": "((day of week)) and ((fave color))",
"id": "bd9caa7e-00ee-4c5a-839e-10ae1a7e6f73",
"name": "personalized",
"redact_personalisation": False,
"subject": None,
"template_type": "sms",
"template_version": 3,
"updated_at": "2024-01-25T23:02:25+00:00",
}
]
"version": 1,
},
"to": "+12133166548",
"updated_at": "2024-01-04T20:43:53+00:00",
}
}
@@ -122,7 +139,7 @@ def test_sign_out_user(
with client_request.session_transaction() as session:
assert session.get("user_id") is not None
# Check we are logged in
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",

View File

@@ -1955,7 +1955,7 @@ def test_set_template_sender(
"sms",
False,
"a" * 160,
"Will be charged as 1 text message",
"Will be charged as 2 text messages",
None,
),
(
@@ -1969,7 +1969,7 @@ def test_set_template_sender(
"sms",
True,
"a" * 147,
"Will be charged as 1 text message",
"Will be charged as 2 text messages",
None,
),
(
@@ -1984,7 +1984,7 @@ def test_set_template_sender(
"sms",
False,
"a" * 918,
"Will be charged as 6 text messages",
"Will be charged as 7 text messages",
None,
),
(
@@ -2025,8 +2025,11 @@ def test_set_template_sender(
(
"sms",
False,
# The length of this string in bytes is 210, needing two fragments.
# Seems like previous calculation was wrong unless the number of characters
# somehow has priority over the number of bytes in a fragment (?)
"" * 70,
"Will be charged as 1 text message. Use of characters outside the IEC_8859-1 character set may increase the message fragment count, resulting in additional charges, and these IEC_8859-1 characters may not display properly on some older mobile devices.", # noqa E501
"Will be charged as 2 text messages",
None,
),
(
@@ -2040,7 +2043,11 @@ def test_set_template_sender(
"sms",
False,
"" * 918,
"Will be charged as 14 text messages",
# The length of this string in bytes is 2754. Divide by 140 and we get 19. Then round up.
# Don't know why it was previously calculated as 14. They seem to have charged by characters
# rather than length of the fragment in bytes.
# "Will be charged as 14 text messages",
"Will be charged as 20 text messages",
None,
),
(

View File

@@ -91,14 +91,14 @@ def get_notifications_csv_mock(
None,
[
"Recipient,Template,Type,Sent by,Job,Carrier,Carrier Response,Status,Time\n",
"foo@bar.com,foo,sms,,,ATT Mobility,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern\r\n",
"foo@bar.com,foo,sms,,,ATT Mobility,Did not like it,Delivered,1943-04-19 08:00:00 US/Eastern\r\n",
],
),
(
"Anne Example",
[
"Recipient,Template,Type,Sent by,Job,Carrier,Carrier Response,Status,Time\n",
"foo@bar.com,foo,sms,Anne Example,,ATT Mobility,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern\r\n", # noqa
"foo@bar.com,foo,sms,Anne Example,,ATT Mobility,Did not like it,Delivered,1943-04-19 08:00:00 US/Eastern\r\n", # noqa
],
),
],
@@ -151,7 +151,7 @@ def test_generate_notifications_csv_without_job(
"ATT Mobility",
"Did not like it",
"Delivered",
"1943-04-19 08:00:00 AM US/Eastern",
"1943-04-19 08:00:00 US/Eastern",
],
),
(
@@ -187,7 +187,7 @@ def test_generate_notifications_csv_without_job(
"ATT Mobility",
"Did not like it",
"Delivered",
"1943-04-19 08:00:00 AM US/Eastern",
"1943-04-19 08:00:00 US/Eastern",
],
),
(
@@ -223,7 +223,7 @@ def test_generate_notifications_csv_without_job(
"ATT Mobility",
"Did not like it",
"Delivered",
"1943-04-19 08:00:00 AM US/Eastern",
"1943-04-19 08:00:00 US/Eastern",
],
),
],
@@ -398,4 +398,4 @@ def test_get_errors_for_csv(
def test_convert_report_date_to_preferred_timezone():
original = "2023-11-16 05:00:00"
altered = convert_report_date_to_preferred_timezone(original)
assert altered == "2023-11-16 12:00:00 AM US/Eastern"
assert altered == "2023-11-16 00:00:00 US/Eastern"