2024-09-03 10:37:05 -07:00
|
|
|
|
import os
|
2023-12-14 13:24:34 -08:00
|
|
|
|
import time
|
|
|
|
|
|
import uuid
|
2023-12-15 10:29:42 -08:00
|
|
|
|
from string import ascii_uppercase
|
2016-05-13 21:25:59 +01:00
|
|
|
|
from zipfile import BadZipFile
|
2016-01-11 15:00:51 +00:00
|
|
|
|
|
2025-04-01 08:00:33 -07:00
|
|
|
|
import bleach
|
2024-06-06 09:34:49 -07:00
|
|
|
|
from flask import (
|
|
|
|
|
|
abort,
|
|
|
|
|
|
current_app,
|
|
|
|
|
|
flash,
|
|
|
|
|
|
redirect,
|
|
|
|
|
|
render_template,
|
|
|
|
|
|
request,
|
|
|
|
|
|
session,
|
|
|
|
|
|
url_for,
|
|
|
|
|
|
)
|
2019-07-01 15:22:08 +01:00
|
|
|
|
from flask_login import current_user
|
2024-03-06 11:49:19 -08:00
|
|
|
|
from markupsafe import Markup
|
2017-06-29 15:31:44 +01:00
|
|
|
|
from notifications_python_client.errors import HTTPError
|
2018-02-20 11:22:17 +00:00
|
|
|
|
from xlrd.biffh import XLRDError
|
Catch exceptions caused by ambiguous Excel files
Excel stores dates as floating point numbers, counting the days (or
fraction thereof) since 1900 (except when it counts from 1904).
However it also, incorrectly, recognises 1900 as a leap year. This means
that it’s ambiguous whether day 59 is February 28th, or February 27th,
depending if you’re counting up or down. In fact any number less than 60
is, therefore, ambiguous.
This shouldn’t really matter since no-one is going to be using dates in
the year 1900 in Notify messages. _Except_ when Excel misidentifies a
cell as containing a date. For example, if you have the number `9`
inside a cell, it means _house number 9_ if the next cell contains
_example_ street. but Excel is all like ‘oh they must want January 9th
1900!’ No. Bad Excel.
There’s not much we can do about this, but we can at least give people
an error message with the workaround, which is what this commit does.
Most of this commit message is paraphrased from:
http://xlrd.readthedocs.io/en/latest/dates.html
2018-02-28 14:45:57 +00:00
|
|
|
|
from xlrd.xldate import XLDateError
|
2015-12-10 21:15:20 +00:00
|
|
|
|
|
2018-02-20 11:22:17 +00:00
|
|
|
|
from app import (
|
|
|
|
|
|
current_service,
|
|
|
|
|
|
job_api_client,
|
2020-01-22 16:13:25 +00:00
|
|
|
|
nl2br,
|
2018-02-20 11:22:17 +00:00
|
|
|
|
notification_api_client,
|
|
|
|
|
|
service_api_client,
|
|
|
|
|
|
)
|
2022-12-05 16:35:46 -05:00
|
|
|
|
from app.main import main
|
2017-05-04 09:30:55 +01:00
|
|
|
|
from app.main.forms import (
|
|
|
|
|
|
ChooseTimeForm,
|
2018-02-20 11:22:17 +00:00
|
|
|
|
CsvUploadForm,
|
2017-10-16 16:35:35 +01:00
|
|
|
|
SetSenderForm,
|
2018-02-20 11:22:17 +00:00
|
|
|
|
get_placeholder_form_instance,
|
2016-01-14 16:00:13 +00:00
|
|
|
|
)
|
2024-08-26 09:02:39 -07:00
|
|
|
|
from app.main.views.user_profile import set_timezone
|
Make user API client return JSON, not a model
The data flow of other bits of our application looks like this:
```
API (returns JSON)
⬇
API client (returns a built in type, usually `dict`)
⬇
Model (returns an instance, eg of type `Service`)
⬇
View (returns HTML)
```
The user API client was architected weirdly, in that it returned a model
directly, like this:
```
API (returns JSON)
⬇
API client (returns a model, of type `User`, `InvitedUser`, etc)
⬇
View (returns HTML)
```
This mixing of different layers of the application is bad because it
makes it hard to write model code that doesn’t have circular
dependencies. As our application gets more complicated we will be
relying more on models to manage this complexity, so we should make it
easy, not hard to write them.
It also means that most of our mocking was of the User model, not just
the underlying JSON. So it would have been easy to introduce subtle bugs
to the user model, because it wasn’t being comprehensively tested. A lot
of the changed lines of code in this commit mean changing the tests to
mock only the JSON, which means that the model layer gets implicitly
tested.
For those reasons this commit changes the user API client to return
JSON, not an instance of `User` or other models.
2019-05-23 15:27:35 +01:00
|
|
|
|
from app.models.user import Users
|
2019-01-30 09:42:15 +00:00
|
|
|
|
from app.s3_client.s3_csv_client import (
|
2020-10-21 13:07:59 +01:00
|
|
|
|
get_csv_metadata,
|
2019-01-30 09:42:15 +00:00
|
|
|
|
s3download,
|
|
|
|
|
|
s3upload,
|
|
|
|
|
|
set_metadata_on_csv_upload,
|
|
|
|
|
|
)
|
2024-06-06 09:34:49 -07:00
|
|
|
|
from app.utils import (
|
|
|
|
|
|
PermanentRedirect,
|
|
|
|
|
|
hilite,
|
|
|
|
|
|
should_skip_template_page,
|
|
|
|
|
|
unicode_truncate,
|
|
|
|
|
|
)
|
2021-06-09 15:15:35 +01:00
|
|
|
|
from app.utils.csv import Spreadsheet, get_errors_for_csv
|
|
|
|
|
|
from app.utils.templates import get_template
|
2021-06-09 13:19:05 +01:00
|
|
|
|
from app.utils.user import user_has_permissions
|
2024-05-16 10:37:37 -04:00
|
|
|
|
from notifications_utils import SMS_CHAR_COUNT_LIMIT
|
|
|
|
|
|
from notifications_utils.insensitive_dict import InsensitiveDict
|
|
|
|
|
|
from notifications_utils.recipients import RecipientCSV, first_column_headings
|
|
|
|
|
|
from notifications_utils.sanitise_text import SanitiseASCII
|
2016-02-22 17:17:18 +00:00
|
|
|
|
|
2016-03-01 15:53:58 +00:00
|
|
|
|
|
2016-05-05 08:39:36 +01:00
|
|
|
|
def get_example_csv_fields(column_headers, use_example_as_example, submitted_fields):
|
|
|
|
|
|
if use_example_as_example:
|
|
|
|
|
|
return ["example" for header in column_headers]
|
|
|
|
|
|
elif submitted_fields:
|
|
|
|
|
|
return [submitted_fields.get(header) for header in column_headers]
|
|
|
|
|
|
else:
|
|
|
|
|
|
return list(column_headers)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_example_csv_rows(template, use_example_as_example=True, submitted_fields=False):
|
2016-11-10 13:39:05 +00:00
|
|
|
|
return {
|
2024-02-29 08:24:53 -08:00
|
|
|
|
"email": (
|
|
|
|
|
|
["test@example.com"]
|
|
|
|
|
|
if use_example_as_example
|
|
|
|
|
|
else [current_user.email_address]
|
|
|
|
|
|
),
|
|
|
|
|
|
"sms": (
|
|
|
|
|
|
["12223334444"] if use_example_as_example else [current_user.mobile_number]
|
|
|
|
|
|
),
|
2018-06-08 12:49:29 +01:00
|
|
|
|
}[template.template_type] + get_example_csv_fields(
|
|
|
|
|
|
(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
placeholder
|
|
|
|
|
|
for placeholder in template.placeholders
|
|
|
|
|
|
if placeholder
|
|
|
|
|
|
not in InsensitiveDict.from_keys(
|
2018-06-08 12:49:29 +01:00
|
|
|
|
first_column_headings[template.template_type]
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
|
|
|
|
|
use_example_as_example,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
submitted_fields,
|
2018-06-08 12:49:29 +01:00
|
|
|
|
)
|
2016-04-01 14:31:28 +01:00
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@main.route(
|
|
|
|
|
|
"/services/<uuid:service_id>/send/<uuid:template_id>/csv", methods=["GET", "POST"]
|
|
|
|
|
|
)
|
|
|
|
|
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
2016-02-22 17:17:18 +00:00
|
|
|
|
def send_messages(service_id, template_id):
|
2023-05-15 14:37:05 -06:00
|
|
|
|
notification_count = service_api_client.get_notification_count(service_id)
|
2023-06-01 15:40:50 -06:00
|
|
|
|
remaining_messages = current_service.message_limit - notification_count
|
2023-05-15 14:37:05 -06:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
db_template = current_service.get_template_with_user_permission_or_403(
|
|
|
|
|
|
template_id, current_user
|
|
|
|
|
|
)
|
2017-05-11 12:01:51 +01:00
|
|
|
|
|
2018-11-06 14:22:22 +00:00
|
|
|
|
email_reply_to = None
|
|
|
|
|
|
sms_sender = None
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if db_template["template_type"] == "email":
|
2018-11-06 14:22:22 +00:00
|
|
|
|
email_reply_to = get_email_reply_to_address_from_session()
|
2023-08-25 09:12:23 -07:00
|
|
|
|
elif db_template["template_type"] == "sms":
|
2018-11-06 14:22:22 +00:00
|
|
|
|
sms_sender = get_sms_sender_from_session()
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if db_template["template_type"] not in current_service.available_template_types:
|
|
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
".action_blocked",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
notification_type=db_template["template_type"],
|
|
|
|
|
|
return_to="view_template",
|
|
|
|
|
|
template_id=template_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2017-06-30 10:55:59 +01:00
|
|
|
|
|
2016-12-08 11:50:59 +00:00
|
|
|
|
template = get_template(
|
2017-06-22 16:00:14 +01:00
|
|
|
|
db_template,
|
2016-12-08 11:50:59 +00:00
|
|
|
|
current_service,
|
2016-12-20 14:38:34 +00:00
|
|
|
|
show_recipient=True,
|
2018-11-06 14:22:22 +00:00
|
|
|
|
email_reply_to=email_reply_to,
|
|
|
|
|
|
sms_sender=sms_sender,
|
2016-03-29 15:59:53 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2016-01-11 15:00:51 +00:00
|
|
|
|
form = CsvUploadForm()
|
2016-01-13 17:32:40 +00:00
|
|
|
|
if form.validate_on_submit():
|
2016-01-11 15:00:51 +00:00
|
|
|
|
try:
|
2016-05-15 07:47:50 +01:00
|
|
|
|
upload_id = s3upload(
|
2016-03-07 18:47:05 +00:00
|
|
|
|
service_id,
|
2020-03-13 13:53:18 +00:00
|
|
|
|
Spreadsheet.from_file_form(form).as_dict,
|
2016-03-07 18:47:05 +00:00
|
|
|
|
)
|
2020-10-23 16:10:03 +01:00
|
|
|
|
file_name_metadata = unicode_truncate(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
SanitiseASCII.encode(form.file.data.filename), 1600
|
2020-10-23 16:10:03 +01:00
|
|
|
|
)
|
|
|
|
|
|
set_metadata_on_csv_upload(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
service_id, upload_id, original_file_name=file_name_metadata
|
|
|
|
|
|
)
|
|
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
".check_messages",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
upload_id=upload_id,
|
|
|
|
|
|
template_id=template.id,
|
|
|
|
|
|
)
|
2020-10-23 16:10:03 +01:00
|
|
|
|
)
|
2016-05-13 21:25:59 +01:00
|
|
|
|
except (UnicodeDecodeError, BadZipFile, XLRDError):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
flash(
|
|
|
|
|
|
"Could not read {}. Try using a different file format.".format(
|
|
|
|
|
|
form.file.data.filename
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
except XLDateError:
|
|
|
|
|
|
flash(
|
|
|
|
|
|
(
|
|
|
|
|
|
"{} contains numbers or dates that Notify cannot understand. "
|
|
|
|
|
|
"Try formatting all columns as ‘text’ or export your file as CSV."
|
|
|
|
|
|
).format(form.file.data.filename)
|
|
|
|
|
|
)
|
2021-12-03 18:14:02 +00:00
|
|
|
|
elif form.errors:
|
|
|
|
|
|
# just show the first error, as we don't expect the form to have more
|
|
|
|
|
|
# than one, since it only has one field
|
|
|
|
|
|
first_field_errors = list(form.errors.values())[0]
|
2025-03-06 15:56:47 -05:00
|
|
|
|
error_message = '<span class="error-message usa-error-message">'
|
2024-03-06 11:49:19 -08:00
|
|
|
|
error_message = f"{error_message}{first_field_errors[0]}"
|
|
|
|
|
|
error_message = f"{error_message}</span>"
|
2025-04-01 08:00:33 -07:00
|
|
|
|
# use 'nosec' because we applied bleach.clean to strip dangerous tags
|
|
|
|
|
|
error_message = bleach.clean(error_message)
|
|
|
|
|
|
error_message = Markup(error_message) # nosec
|
2024-03-06 11:49:19 -08:00
|
|
|
|
flash(error_message)
|
2018-06-08 12:49:29 +01:00
|
|
|
|
column_headings = get_spreadsheet_column_headings_from_template(template)
|
2016-11-10 13:39:05 +00:00
|
|
|
|
|
2016-02-08 16:36:53 +00:00
|
|
|
|
return render_template(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"views/send.html",
|
2016-02-08 16:36:53 +00:00
|
|
|
|
template=template,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
column_headings=list(ascii_uppercase[: len(column_headings)]),
|
2016-11-10 13:39:05 +00:00
|
|
|
|
example=[column_headings, get_example_csv_rows(template)],
|
2020-12-16 11:05:19 +00:00
|
|
|
|
form=form,
|
2023-05-15 14:37:05 -06:00
|
|
|
|
allowed_file_extensions=Spreadsheet.ALLOWED_FILE_EXTENSIONS,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
remaining_messages=remaining_messages,
|
2016-02-08 16:36:53 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@main.route("/services/<uuid:service_id>/send/<uuid:template_id>.csv", methods=["GET"])
|
|
|
|
|
|
@user_has_permissions("send_messages", "manage_templates")
|
2016-05-05 08:39:36 +01:00
|
|
|
|
def get_example_csv(service_id, template_id):
|
2016-12-08 11:50:59 +00:00
|
|
|
|
template = get_template(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
service_api_client.get_service_template(service_id, template_id)["data"],
|
|
|
|
|
|
current_service,
|
|
|
|
|
|
)
|
|
|
|
|
|
return (
|
|
|
|
|
|
Spreadsheet.from_rows(
|
|
|
|
|
|
[
|
|
|
|
|
|
get_spreadsheet_column_headings_from_template(template),
|
|
|
|
|
|
get_example_csv_rows(template),
|
|
|
|
|
|
]
|
|
|
|
|
|
).as_csv_data,
|
|
|
|
|
|
200,
|
|
|
|
|
|
{
|
|
|
|
|
|
"Content-Type": "text/csv; charset=utf-8",
|
|
|
|
|
|
"Content-Disposition": 'inline; filename="{}.csv"'.format(template.name),
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2015-12-10 21:15:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@main.route(
|
|
|
|
|
|
"/services/<uuid:service_id>/send/<uuid:template_id>/set-sender",
|
|
|
|
|
|
methods=["GET", "POST"],
|
|
|
|
|
|
)
|
|
|
|
|
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
2017-10-16 16:35:35 +01:00
|
|
|
|
def set_sender(service_id, template_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
session["sender_id"] = None
|
2017-10-16 16:35:35 +01:00
|
|
|
|
redirect_to_one_off = redirect(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
url_for(".send_one_off", service_id=service_id, template_id=template_id)
|
2017-10-16 16:35:35 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
template = current_service.get_template_with_user_permission_or_403(
|
|
|
|
|
|
template_id, current_user
|
|
|
|
|
|
)
|
2017-10-16 16:35:35 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
sender_details = get_sender_details(service_id, template["template_type"])
|
2021-02-08 17:23:47 +00:00
|
|
|
|
|
2023-10-06 11:41:43 -06:00
|
|
|
|
sender_details = remove_notify_from_sender_options(sender_details)
|
|
|
|
|
|
|
2021-02-08 17:23:47 +00:00
|
|
|
|
if len(sender_details) == 1:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
session["sender_id"] = sender_details[0]["id"]
|
2021-02-08 17:23:47 +00:00
|
|
|
|
|
2017-10-16 16:35:35 +01:00
|
|
|
|
if len(sender_details) <= 1:
|
|
|
|
|
|
return redirect_to_one_off
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
sender_context = get_sender_context(sender_details, template["template_type"])
|
2017-10-16 16:35:35 +01:00
|
|
|
|
|
|
|
|
|
|
form = SetSenderForm(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
sender=sender_context["default_id"],
|
|
|
|
|
|
sender_choices=sender_context["value_and_label"],
|
|
|
|
|
|
sender_label=sender_context["description"],
|
2017-10-16 16:35:35 +01:00
|
|
|
|
)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
option_hints = {sender_context["default_id"]: "(Default)"}
|
|
|
|
|
|
if sender_context.get("receives_text_message", None):
|
|
|
|
|
|
option_hints.update(
|
|
|
|
|
|
{sender_context["receives_text_message"]: "(Receives replies)"}
|
|
|
|
|
|
)
|
|
|
|
|
|
if sender_context.get("default_and_receives", None):
|
|
|
|
|
|
option_hints = {
|
|
|
|
|
|
sender_context["default_and_receives"]: "(Default and receives replies)"
|
|
|
|
|
|
}
|
2017-10-16 16:35:35 +01:00
|
|
|
|
|
2021-01-13 10:31:07 +00:00
|
|
|
|
# extend all radios that need hint text
|
2023-08-25 09:12:23 -07:00
|
|
|
|
form.sender.param_extensions = {"items": []}
|
2021-01-13 10:31:07 +00:00
|
|
|
|
for item_id, _item_value in form.sender.choices:
|
|
|
|
|
|
if item_id in option_hints:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
extensions = {"hint": {"text": option_hints[item_id]}}
|
2021-01-13 10:31:07 +00:00
|
|
|
|
else:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
extensions = (
|
|
|
|
|
|
{}
|
|
|
|
|
|
) # if no extensions needed, send an empty dict to preserve order of items
|
|
|
|
|
|
form.sender.param_extensions["items"].append(extensions)
|
2021-01-13 10:31:07 +00:00
|
|
|
|
|
2017-10-16 16:35:35 +01:00
|
|
|
|
if form.validate_on_submit():
|
2023-08-25 09:12:23 -07:00
|
|
|
|
session["sender_id"] = form.sender.data
|
|
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(".send_one_off", service_id=service_id, template_id=template_id)
|
|
|
|
|
|
)
|
2017-10-16 16:35:35 +01:00
|
|
|
|
|
|
|
|
|
|
return render_template(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"views/templates/set-sender.html",
|
2017-10-16 16:35:35 +01:00
|
|
|
|
form=form,
|
|
|
|
|
|
template_id=template_id,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
sender_context={
|
|
|
|
|
|
"title": sender_context["title"],
|
|
|
|
|
|
"description": sender_context["description"],
|
|
|
|
|
|
},
|
|
|
|
|
|
option_hints=option_hints,
|
2017-10-16 16:35:35 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-10-06 11:41:43 -06:00
|
|
|
|
def remove_notify_from_sender_options(sender_details):
|
2023-11-17 11:19:26 -07:00
|
|
|
|
# Remove US Notify/Notify.gov from users list of sender
|
|
|
|
|
|
# options during message send flow
|
|
|
|
|
|
sender_details = [
|
2023-11-20 10:39:39 -07:00
|
|
|
|
sender for sender in sender_details if verify_sender_options(sender)
|
2023-11-17 11:19:26 -07:00
|
|
|
|
]
|
|
|
|
|
|
|
2023-10-06 11:41:43 -06:00
|
|
|
|
return sender_details
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-11-20 10:39:39 -07:00
|
|
|
|
def verify_sender_options(sender):
|
2023-11-22 11:55:40 -07:00
|
|
|
|
if sender.get("sms_sender") in ["Notify.gov", "US Notify"] and sender["is_default"]:
|
|
|
|
|
|
return True
|
2023-11-22 11:06:15 -07:00
|
|
|
|
if sender.get("sms_sender") not in ["Notify.gov", "US Notify"]:
|
2023-11-20 10:39:39 -07:00
|
|
|
|
return True
|
|
|
|
|
|
|
2023-11-22 11:06:15 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
2023-11-20 10:39:39 -07:00
|
|
|
|
|
2017-10-16 16:35:35 +01:00
|
|
|
|
def get_sender_context(sender_details, template_type):
|
|
|
|
|
|
context = {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"email": {
|
|
|
|
|
|
"title": "Where should replies come back to?",
|
|
|
|
|
|
"description": "Where should replies come back to?",
|
|
|
|
|
|
"field_name": "email_address",
|
|
|
|
|
|
},
|
|
|
|
|
|
"sms": {
|
|
|
|
|
|
"title": "Who should the message come from?",
|
|
|
|
|
|
"description": "Who should the message come from?",
|
|
|
|
|
|
"field_name": "sms_sender",
|
2017-10-16 16:35:35 +01:00
|
|
|
|
},
|
|
|
|
|
|
}[template_type]
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
sender_format = context["field_name"]
|
2017-10-16 16:35:35 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
context["default_id"] = next(
|
|
|
|
|
|
sender["id"] for sender in sender_details if sender["is_default"]
|
|
|
|
|
|
)
|
|
|
|
|
|
if template_type == "sms":
|
|
|
|
|
|
inbound = [
|
|
|
|
|
|
sender["id"] for sender in sender_details if sender["inbound_number_id"]
|
|
|
|
|
|
]
|
2017-11-02 15:48:19 +00:00
|
|
|
|
if inbound:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
context["receives_text_message"] = next(iter(inbound))
|
|
|
|
|
|
if context["default_id"] == context.get("receives_text_message", None):
|
|
|
|
|
|
context["default_and_receives"] = context["default_id"]
|
2017-11-02 15:48:19 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
context["value_and_label"] = [
|
|
|
|
|
|
(sender["id"], nl2br(sender[sender_format])) for sender in sender_details
|
|
|
|
|
|
]
|
2017-10-16 16:35:35 +01:00
|
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_sender_details(service_id, template_type):
|
|
|
|
|
|
api_call = {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"email": service_api_client.get_reply_to_email_addresses,
|
|
|
|
|
|
"sms": service_api_client.get_sms_senders,
|
2017-10-16 16:35:35 +01:00
|
|
|
|
}[template_type]
|
|
|
|
|
|
return api_call(service_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-10-07 14:51:18 +01:00
|
|
|
|
@main.route("/services/<uuid:service_id>/send/<uuid:template_id>/one-off")
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
2020-10-07 14:51:18 +01:00
|
|
|
|
def send_one_off(service_id, template_id):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
session["recipient"] = None
|
|
|
|
|
|
session["placeholders"] = {}
|
|
|
|
|
|
|
|
|
|
|
|
db_template = current_service.get_template_with_user_permission_or_403(
|
|
|
|
|
|
template_id, current_user
|
|
|
|
|
|
)
|
2017-06-30 10:55:59 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if db_template["template_type"] not in current_service.available_template_types:
|
|
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
".action_blocked",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
notification_type=db_template["template_type"],
|
|
|
|
|
|
return_to="view_template",
|
|
|
|
|
|
template_id=template_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2017-06-30 10:55:59 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
".send_one_off_step",
|
2017-06-30 10:55:59 +01:00
|
|
|
|
service_id=service_id,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
template_id=template_id,
|
|
|
|
|
|
step_index=0,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2017-05-04 09:30:55 +01:00
|
|
|
|
|
2016-04-03 11:30:23 +01:00
|
|
|
|
|
2017-06-23 17:30:34 +01:00
|
|
|
|
def get_notification_check_endpoint(service_id, template):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
"main.check_notification",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
template_id=template.id,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2017-06-23 17:30:34 +01:00
|
|
|
|
|
|
|
|
|
|
|
2017-05-25 08:55:05 +01:00
|
|
|
|
@main.route(
|
2019-11-04 11:07:55 +00:00
|
|
|
|
"/services/<uuid:service_id>/send/<uuid:template_id>/one-off/step-<int:step_index>",
|
2023-08-25 09:12:23 -07:00
|
|
|
|
methods=["GET", "POST"],
|
2017-05-25 08:55:05 +01:00
|
|
|
|
)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
2020-10-07 14:51:18 +01:00
|
|
|
|
def send_one_off_step(service_id, template_id, step_index):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if {"recipient", "placeholders"} - set(session.keys()):
|
|
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
".send_one_off",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
template_id=template_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2017-05-22 11:49:15 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
db_template = current_service.get_template_with_user_permission_or_403(
|
|
|
|
|
|
template_id, current_user
|
|
|
|
|
|
)
|
2017-05-11 12:01:51 +01:00
|
|
|
|
|
2017-11-02 12:07:46 +00:00
|
|
|
|
email_reply_to = None
|
|
|
|
|
|
sms_sender = None
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if db_template["template_type"] == "email":
|
2018-10-26 17:31:49 +01:00
|
|
|
|
email_reply_to = get_email_reply_to_address_from_session()
|
2023-08-25 09:12:23 -07:00
|
|
|
|
elif db_template["template_type"] == "sms":
|
|
|
|
|
|
sms_sender = (
|
|
|
|
|
|
get_sms_sender_from_session()
|
2023-08-31 08:21:28 -07:00
|
|
|
|
) # TODO: verify default sender is Notify.gov
|
2020-07-21 16:06:33 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
template_values = get_recipient_and_placeholders_from_session(
|
|
|
|
|
|
db_template["template_type"]
|
|
|
|
|
|
)
|
2020-07-21 16:06:33 +01:00
|
|
|
|
|
2016-12-08 11:50:59 +00:00
|
|
|
|
template = get_template(
|
2017-06-22 16:00:14 +01:00
|
|
|
|
db_template,
|
2016-12-08 11:50:59 +00:00
|
|
|
|
current_service,
|
2016-12-20 14:38:34 +00:00
|
|
|
|
show_recipient=True,
|
2017-11-02 12:07:46 +00:00
|
|
|
|
email_reply_to=email_reply_to,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
sms_sender=sms_sender,
|
2016-05-05 08:39:36 +01:00
|
|
|
|
)
|
2016-03-29 15:59:53 +01:00
|
|
|
|
|
2020-10-07 14:51:18 +01:00
|
|
|
|
placeholders = fields_to_fill_in(template)
|
2017-05-04 09:30:55 +01:00
|
|
|
|
|
2017-05-22 11:49:15 +01:00
|
|
|
|
try:
|
2019-06-06 12:24:02 +01:00
|
|
|
|
current_placeholder = placeholders[step_index]
|
2017-05-22 11:49:15 +01:00
|
|
|
|
except IndexError:
|
2017-05-25 09:18:26 +01:00
|
|
|
|
if all_placeholders_in_session(placeholders):
|
2017-06-23 17:30:34 +01:00
|
|
|
|
return get_notification_check_endpoint(service_id, template)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
".send_one_off",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
template_id=template_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2017-06-22 16:00:14 +01:00
|
|
|
|
|
2017-05-04 09:30:55 +01:00
|
|
|
|
form = get_placeholder_form_instance(
|
|
|
|
|
|
current_placeholder,
|
2017-06-23 17:30:34 +01:00
|
|
|
|
dict_to_populate_from=get_normalised_placeholders_from_session(),
|
2018-03-16 14:17:43 +00:00
|
|
|
|
template_type=template.template_type,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
allow_international_phone_numbers=current_service.has_permission(
|
|
|
|
|
|
"international_sms"
|
|
|
|
|
|
),
|
2017-05-04 09:30:55 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if form.validate_on_submit():
|
2017-06-26 10:58:43 +01:00
|
|
|
|
# if it's the first input (phone/email), we store against `recipient` as well, for easier extraction.
|
2022-12-05 15:33:44 -05:00
|
|
|
|
# Only if we're not on the test route, since that will already have the user's own number set
|
|
|
|
|
|
if step_index == 0:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
session["recipient"] = form.placeholder_value.data
|
2017-05-04 09:30:55 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
session["placeholders"][current_placeholder] = form.placeholder_value.data
|
2017-05-04 09:30:55 +01:00
|
|
|
|
|
2017-05-25 09:18:26 +01:00
|
|
|
|
if all_placeholders_in_session(placeholders):
|
2017-06-23 17:30:34 +01:00
|
|
|
|
return get_notification_check_endpoint(service_id, template)
|
2017-05-04 09:30:55 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
request.endpoint,
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
template_id=template_id,
|
|
|
|
|
|
step_index=step_index + 1,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2016-02-12 10:55:21 +00:00
|
|
|
|
|
2020-04-09 16:25:48 +01:00
|
|
|
|
back_link = get_back_link(service_id, template, step_index, placeholders)
|
2017-05-04 09:30:55 +01:00
|
|
|
|
|
2020-07-21 16:06:33 +01:00
|
|
|
|
template.values = template_values
|
2017-05-16 17:17:11 +01:00
|
|
|
|
template.values[current_placeholder] = None
|
2017-05-04 09:30:55 +01:00
|
|
|
|
|
2016-05-05 08:39:36 +01:00
|
|
|
|
return render_template(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"views/send-test.html",
|
2017-11-20 16:45:59 +00:00
|
|
|
|
page_title=get_send_test_page_title(
|
|
|
|
|
|
template.template_type,
|
2025-04-25 16:20:21 -04:00
|
|
|
|
entering_recipient=not session["recipient"],
|
2018-08-06 11:20:50 +01:00
|
|
|
|
name=template.name,
|
2017-11-20 16:45:59 +00:00
|
|
|
|
),
|
2016-05-05 08:39:36 +01:00
|
|
|
|
template=template,
|
2017-05-04 09:30:55 +01:00
|
|
|
|
form=form,
|
2025-04-21 12:12:09 -04:00
|
|
|
|
current_placeholder=current_placeholder,
|
2020-04-09 16:37:38 +01:00
|
|
|
|
skip_link=get_skip_link(step_index, template),
|
2017-05-04 09:30:55 +01:00
|
|
|
|
back_link=back_link,
|
2018-07-18 09:48:19 +01:00
|
|
|
|
link_to_upload=(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
request.endpoint == "main.send_one_off_step" and step_index == 0
|
2018-07-18 09:48:19 +01:00
|
|
|
|
),
|
2024-12-20 14:04:25 -05:00
|
|
|
|
errors=form.errors if form.errors else None,
|
2016-05-05 08:39:36 +01:00
|
|
|
|
)
|
2016-02-12 10:55:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
2024-04-01 18:03:38 -07:00
|
|
|
|
def _check_messages(service_id, template_id, upload_id, preview_row, **kwargs):
|
2018-05-03 13:35:59 +01:00
|
|
|
|
try:
|
2018-04-30 12:55:56 +01:00
|
|
|
|
# The happy path is that the job doesn’t already exist, so the
|
|
|
|
|
|
# API will return a 404 and the client will raise HTTPError.
|
|
|
|
|
|
job_api_client.get_job(service_id, upload_id)
|
2022-08-05 00:25:03 -07:00
|
|
|
|
|
2018-05-03 13:35:59 +01:00
|
|
|
|
# the job exists already - so go back to the templates page
|
2018-04-30 12:55:56 +01:00
|
|
|
|
# If we just return a `redirect` (302) object here, we'll get
|
|
|
|
|
|
# errors when we try and unpack in the check_messages route.
|
|
|
|
|
|
# Rasing a werkzeug.routing redirect means that doesn't happen.
|
2023-08-25 09:12:23 -07:00
|
|
|
|
raise PermanentRedirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
"main.send_messages", service_id=service_id, template_id=template_id
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2018-05-03 13:35:59 +01:00
|
|
|
|
except HTTPError as e:
|
|
|
|
|
|
if e.status_code != 404:
|
|
|
|
|
|
raise
|
2018-04-30 12:55:56 +01:00
|
|
|
|
|
2021-06-23 15:56:05 +01:00
|
|
|
|
notification_count = service_api_client.get_notification_count(service_id)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
remaining_messages = current_service.message_limit - notification_count
|
2016-04-04 14:28:52 +01:00
|
|
|
|
|
2016-03-07 18:47:05 +00:00
|
|
|
|
contents = s3download(service_id, upload_id)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
db_template = current_service.get_template_with_user_permission_or_403(
|
|
|
|
|
|
template_id, current_user
|
|
|
|
|
|
)
|
2018-03-29 17:01:35 +01:00
|
|
|
|
|
2017-11-02 12:07:46 +00:00
|
|
|
|
email_reply_to = None
|
|
|
|
|
|
sms_sender = None
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if db_template["template_type"] == "email":
|
2018-10-26 17:31:49 +01:00
|
|
|
|
email_reply_to = get_email_reply_to_address_from_session()
|
2023-08-25 09:12:23 -07:00
|
|
|
|
elif db_template["template_type"] == "sms":
|
2018-10-26 17:31:49 +01:00
|
|
|
|
sms_sender = get_sms_sender_from_session()
|
2019-10-08 14:56:00 +01:00
|
|
|
|
|
2016-12-08 11:50:59 +00:00
|
|
|
|
template = get_template(
|
2018-05-10 17:00:41 +01:00
|
|
|
|
db_template,
|
2016-12-08 11:50:59 +00:00
|
|
|
|
current_service,
|
2024-02-13 16:18:50 -08:00
|
|
|
|
show_recipient=False,
|
2017-11-02 12:07:46 +00:00
|
|
|
|
email_reply_to=email_reply_to,
|
2018-03-29 17:01:35 +01:00
|
|
|
|
sms_sender=sms_sender,
|
2024-04-01 18:03:38 -07:00
|
|
|
|
**kwargs,
|
2024-02-16 17:19:03 -08:00
|
|
|
|
)
|
2024-03-13 16:31:21 -06:00
|
|
|
|
|
|
|
|
|
|
allow_list = []
|
|
|
|
|
|
if current_service.trial_mode:
|
|
|
|
|
|
# Adding the simulated numbers to allow list
|
|
|
|
|
|
# so they can be sent in trial mode
|
|
|
|
|
|
for user in Users(service_id):
|
|
|
|
|
|
allow_list.extend([user.name, user.mobile_number, user.email_address])
|
|
|
|
|
|
# Failed sms number
|
2024-03-14 13:26:05 -07:00
|
|
|
|
allow_list.extend(
|
|
|
|
|
|
["simulated user (fail)", "+14254147167", "simulated@simulated.gov"]
|
|
|
|
|
|
)
|
2024-03-13 16:31:21 -06:00
|
|
|
|
# Success sms number
|
|
|
|
|
|
allow_list.extend(
|
2024-03-14 10:16:56 -06:00
|
|
|
|
["simulated user (success)", "+14254147755", "simulatedtwo@simulated.gov"]
|
2024-03-13 16:31:21 -06:00
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
allow_list = None
|
2016-03-07 18:47:05 +00:00
|
|
|
|
recipients = RecipientCSV(
|
|
|
|
|
|
contents,
|
2024-04-01 18:03:38 -07:00
|
|
|
|
template=template,
|
2016-04-07 14:30:51 +01:00
|
|
|
|
max_initial_rows_shown=50,
|
|
|
|
|
|
max_errors_shown=50,
|
2024-03-13 16:31:21 -06:00
|
|
|
|
guestlist=allow_list,
|
2017-04-26 16:19:49 +01:00
|
|
|
|
remaining_messages=remaining_messages,
|
2023-08-25 09:12:23 -07:00
|
|
|
|
allow_international_sms=current_service.has_permission("international_sms"),
|
2016-02-26 10:54:06 +00:00
|
|
|
|
)
|
2016-03-07 18:47:05 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if request.args.get("from_test"):
|
2025-04-25 16:20:21 -04:00
|
|
|
|
back_link = {
|
|
|
|
|
|
"href": {
|
|
|
|
|
|
"url": url_for(
|
|
|
|
|
|
"main.send_one_off", service_id=service_id, template_id=template.id
|
|
|
|
|
|
),
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"text": "Back to message personalization",
|
2025-04-25 16:20:21 -04:00
|
|
|
|
},
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"html": "Back to message personalization",
|
2025-04-25 16:20:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
back_link_from_preview = {
|
|
|
|
|
|
"href": {
|
|
|
|
|
|
"url": url_for(
|
|
|
|
|
|
"main.send_one_off", service_id=service_id, template_id=template.id
|
|
|
|
|
|
),
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"text": "Back to message personalization",
|
2025-04-25 16:20:21 -04:00
|
|
|
|
},
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"html": "Back to message personalization",
|
2025-04-25 16:20:21 -04:00
|
|
|
|
}
|
2016-08-30 09:27:24 +01:00
|
|
|
|
choose_time_form = None
|
2016-05-05 08:39:36 +01:00
|
|
|
|
else:
|
2025-04-25 16:20:21 -04:00
|
|
|
|
back_link = {
|
|
|
|
|
|
"href": {
|
|
|
|
|
|
"url": url_for(
|
|
|
|
|
|
"main.send_messages", service_id=service_id, template_id=template.id
|
|
|
|
|
|
),
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"text": "Back to upload a file",
|
2025-04-25 16:20:21 -04:00
|
|
|
|
},
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"html": "Back to upload a file",
|
2025-04-25 16:20:21 -04:00
|
|
|
|
}
|
|
|
|
|
|
back_link_from_preview = {
|
|
|
|
|
|
"href": {
|
|
|
|
|
|
"url": url_for(
|
|
|
|
|
|
"main.check_messages",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
template_id=template.id,
|
|
|
|
|
|
upload_id=upload_id,
|
|
|
|
|
|
),
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"text": "Back to check messages",
|
2025-04-25 16:20:21 -04:00
|
|
|
|
},
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"html": "Back to check messages",
|
2025-04-25 16:20:21 -04:00
|
|
|
|
}
|
2016-08-30 09:27:24 +01:00
|
|
|
|
choose_time_form = ChooseTimeForm()
|
2016-05-05 08:39:36 +01:00
|
|
|
|
|
2018-01-15 11:35:45 +00:00
|
|
|
|
if preview_row < 2:
|
|
|
|
|
|
abort(404)
|
|
|
|
|
|
|
2018-03-05 15:57:10 +00:00
|
|
|
|
if preview_row < len(recipients) + 2:
|
|
|
|
|
|
template.values = recipients[preview_row - 2].recipient_and_personalisation
|
2018-01-15 11:35:45 +00:00
|
|
|
|
elif preview_row > 2:
|
2017-12-20 11:48:30 +00:00
|
|
|
|
abort(404)
|
2016-03-07 18:47:05 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
original_file_name = get_csv_metadata(service_id, upload_id).get(
|
|
|
|
|
|
"original_file_name", ""
|
|
|
|
|
|
)
|
2019-10-08 14:56:00 +01:00
|
|
|
|
|
2016-12-20 11:56:22 +00:00
|
|
|
|
return dict(
|
2016-03-07 18:47:05 +00:00
|
|
|
|
recipients=recipients,
|
|
|
|
|
|
template=template,
|
2016-06-03 13:52:15 +01:00
|
|
|
|
errors=recipients.has_errors,
|
|
|
|
|
|
row_errors=get_errors_for_csv(recipients, template.template_type),
|
2018-03-05 15:57:10 +00:00
|
|
|
|
count_of_recipients=len(recipients),
|
|
|
|
|
|
count_of_displayed_recipients=len(list(recipients.displayed_rows)),
|
2020-10-21 13:07:59 +01:00
|
|
|
|
original_file_name=original_file_name,
|
2016-03-29 15:59:53 +01:00
|
|
|
|
upload_id=upload_id,
|
2016-04-22 09:36:42 +01:00
|
|
|
|
form=CsvUploadForm(),
|
2016-07-25 14:16:34 +01:00
|
|
|
|
remaining_messages=remaining_messages,
|
2016-08-30 09:27:24 +01:00
|
|
|
|
choose_time_form=choose_time_form,
|
2016-07-05 11:39:07 +01:00
|
|
|
|
back_link=back_link,
|
2024-02-06 16:08:14 -08:00
|
|
|
|
back_link_from_preview=back_link_from_preview,
|
2020-04-19 22:46:13 +01:00
|
|
|
|
first_recipient_column=recipients.recipient_column_headers[0],
|
2018-01-15 11:25:11 +00:00
|
|
|
|
preview_row=preview_row,
|
2019-02-04 14:08:11 +00:00
|
|
|
|
sent_previously=job_api_client.has_sent_previously(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
service_id, template.id, db_template["version"], original_file_name
|
2019-10-01 15:34:46 +01:00
|
|
|
|
),
|
2024-02-08 18:43:17 -08:00
|
|
|
|
template_id=template_id,
|
2016-03-07 18:47:05 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@main.route(
|
|
|
|
|
|
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>",
|
|
|
|
|
|
methods=["GET"],
|
|
|
|
|
|
)
|
2019-11-29 13:03:23 +00:00
|
|
|
|
@main.route(
|
|
|
|
|
|
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>/row-<int:row_index>",
|
2023-08-25 09:12:23 -07:00
|
|
|
|
methods=["GET"],
|
2019-11-29 13:03:23 +00:00
|
|
|
|
)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
2018-03-29 17:01:35 +01:00
|
|
|
|
def check_messages(service_id, template_id, upload_id, row_index=2):
|
2018-05-11 08:46:42 +01:00
|
|
|
|
data = _check_messages(service_id, template_id, upload_id, row_index)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data["allowed_file_extensions"] = Spreadsheet.ALLOWED_FILE_EXTENSIONS
|
2017-07-20 08:59:49 +01:00
|
|
|
|
|
2017-07-20 09:10:23 +01:00
|
|
|
|
if (
|
2023-08-25 09:12:23 -07:00
|
|
|
|
data["recipients"].too_many_rows
|
|
|
|
|
|
or not data["count_of_recipients"]
|
|
|
|
|
|
or not data["recipients"].has_recipient_columns
|
|
|
|
|
|
or data["recipients"].duplicate_recipient_column_headers
|
|
|
|
|
|
or data["recipients"].missing_column_headers
|
|
|
|
|
|
or data["sent_previously"]
|
2017-07-20 09:10:23 +01:00
|
|
|
|
):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return render_template("views/check/column-errors.html", **data)
|
2017-07-20 09:10:23 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if data["row_errors"]:
|
|
|
|
|
|
return render_template("views/check/row-errors.html", **data)
|
2017-07-20 08:59:49 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if data["errors"]:
|
|
|
|
|
|
return render_template("views/check/column-errors.html", **data)
|
2017-07-20 08:59:49 +01:00
|
|
|
|
|
2018-11-06 14:22:22 +00:00
|
|
|
|
metadata_kwargs = {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"notification_count": data["count_of_recipients"],
|
|
|
|
|
|
"template_id": template_id,
|
|
|
|
|
|
"valid": True,
|
|
|
|
|
|
"original_file_name": data.get("original_file_name", ""),
|
2018-11-06 14:22:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if session.get("sender_id"):
|
|
|
|
|
|
metadata_kwargs["sender_id"] = session["sender_id"]
|
2018-11-06 14:22:22 +00:00
|
|
|
|
|
|
|
|
|
|
set_metadata_on_csv_upload(service_id, upload_id, **metadata_kwargs)
|
2018-05-11 08:46:42 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return render_template("views/check/ok.html", **data)
|
2016-12-20 11:56:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
2024-02-02 14:52:48 -08:00
|
|
|
|
@main.route(
|
|
|
|
|
|
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>/preview",
|
2024-03-08 15:05:44 -08:00
|
|
|
|
methods=["POST"],
|
2024-02-08 18:43:17 -08:00
|
|
|
|
)
|
|
|
|
|
|
@main.route(
|
|
|
|
|
|
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>/preview/row-<int:row_index>",
|
2024-03-08 15:05:44 -08:00
|
|
|
|
methods=["POST"],
|
2024-02-02 14:52:48 -08:00
|
|
|
|
)
|
|
|
|
|
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
|
|
|
|
|
def preview_job(service_id, template_id, upload_id, row_index=2):
|
2024-03-08 15:05:44 -08:00
|
|
|
|
session["scheduled_for"] = request.form.get("scheduled_for", "")
|
2024-04-09 12:52:45 -07:00
|
|
|
|
data = _check_messages(
|
|
|
|
|
|
service_id, template_id, upload_id, row_index, force_hide_sender=True
|
|
|
|
|
|
)
|
2024-02-02 14:52:48 -08:00
|
|
|
|
|
2024-02-08 18:43:17 -08:00
|
|
|
|
return render_template(
|
2024-02-16 17:19:03 -08:00
|
|
|
|
"views/check/preview.html",
|
|
|
|
|
|
scheduled_for=session["scheduled_for"],
|
|
|
|
|
|
**data,
|
2024-02-08 18:43:17 -08:00
|
|
|
|
)
|
2024-02-06 17:06:18 -08:00
|
|
|
|
|
2024-02-02 14:52:48 -08:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@main.route("/services/<uuid:service_id>/start-job/<uuid:upload_id>", methods=["POST"])
|
|
|
|
|
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
2016-03-07 18:47:05 +00:00
|
|
|
|
def start_job(service_id, upload_id):
|
2024-02-08 18:43:17 -08:00
|
|
|
|
scheduled_for = session.pop("scheduled_for", None)
|
2016-03-08 17:50:18 +00:00
|
|
|
|
job_api_client.create_job(
|
|
|
|
|
|
upload_id,
|
|
|
|
|
|
service_id,
|
2024-02-06 16:08:14 -08:00
|
|
|
|
scheduled_for=scheduled_for,
|
2016-03-08 17:50:18 +00:00
|
|
|
|
)
|
2016-03-07 18:47:05 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
session.pop("sender_id", None)
|
2018-11-06 14:22:22 +00:00
|
|
|
|
|
2016-03-07 18:47:05 +00:00
|
|
|
|
return redirect(
|
2017-07-13 11:28:37 +01:00
|
|
|
|
url_for(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"main.view_job",
|
2017-07-13 11:28:37 +01:00
|
|
|
|
job_id=upload_id,
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
)
|
2016-02-17 15:49:07 +00:00
|
|
|
|
)
|
2016-07-01 10:49:54 +01:00
|
|
|
|
|
|
|
|
|
|
|
2017-05-25 09:18:26 +01:00
|
|
|
|
def fields_to_fill_in(template, prefill_current_user=False):
|
2020-04-27 10:57:25 +01:00
|
|
|
|
if not prefill_current_user:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return first_column_headings[template.template_type] + list(
|
|
|
|
|
|
template.placeholders
|
|
|
|
|
|
)
|
2017-05-04 09:30:55 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if template.template_type == "sms":
|
|
|
|
|
|
session["recipient"] = current_user.mobile_number
|
|
|
|
|
|
session["placeholders"]["phone number"] = current_user.mobile_number
|
2019-06-04 16:12:09 +01:00
|
|
|
|
else:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
session["recipient"] = current_user.email_address
|
|
|
|
|
|
session["placeholders"]["email address"] = current_user.email_address
|
2017-05-04 09:30:55 +01:00
|
|
|
|
|
|
|
|
|
|
return list(template.placeholders)
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-06-23 17:30:34 +01:00
|
|
|
|
def get_normalised_placeholders_from_session():
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return InsensitiveDict(session.get("placeholders", {}))
|
2017-05-04 09:30:55 +01:00
|
|
|
|
|
|
|
|
|
|
|
2017-06-29 15:31:44 +01:00
|
|
|
|
def get_recipient_and_placeholders_from_session(template_type):
|
2017-06-26 14:56:14 +01:00
|
|
|
|
placeholders = get_normalised_placeholders_from_session()
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if template_type == "sms":
|
|
|
|
|
|
placeholders["phone_number"] = session["recipient"]
|
2017-06-26 14:56:14 +01:00
|
|
|
|
else:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
placeholders["email_address"] = session["recipient"]
|
2017-06-26 14:56:14 +01:00
|
|
|
|
|
|
|
|
|
|
return placeholders
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-05-25 09:18:26 +01:00
|
|
|
|
def all_placeholders_in_session(placeholders):
|
|
|
|
|
|
return all(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
get_normalised_placeholders_from_session().get(placeholder, False)
|
|
|
|
|
|
not in (False, None)
|
2017-05-25 09:18:26 +01:00
|
|
|
|
for placeholder in placeholders
|
|
|
|
|
|
)
|
2017-05-25 13:31:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
2020-10-05 11:55:15 +01:00
|
|
|
|
def get_send_test_page_title(template_type, entering_recipient, name=None):
|
2017-11-20 16:45:59 +00:00
|
|
|
|
if entering_recipient:
|
2024-11-22 16:45:25 -05:00
|
|
|
|
return "Select recipients"
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return "Personalize this message"
|
2017-06-22 16:00:14 +01:00
|
|
|
|
|
|
|
|
|
|
|
2024-02-08 18:43:17 -08:00
|
|
|
|
def get_back_link(
|
|
|
|
|
|
service_id,
|
|
|
|
|
|
template,
|
|
|
|
|
|
step_index,
|
|
|
|
|
|
placeholders=None,
|
|
|
|
|
|
preview=False,
|
|
|
|
|
|
):
|
2024-02-06 16:08:14 -08:00
|
|
|
|
if preview:
|
2025-04-25 10:35:25 -04:00
|
|
|
|
return {
|
2025-04-25 10:23:28 -04:00
|
|
|
|
"href": {
|
|
|
|
|
|
"url": url_for(
|
|
|
|
|
|
"main.check_notification",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
template_id=template.id,
|
|
|
|
|
|
),
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"text": "Back to select delivery time",
|
2025-04-25 10:23:28 -04:00
|
|
|
|
},
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"html": "Back to select delivery time",
|
2025-04-25 10:23:28 -04:00
|
|
|
|
}
|
2024-02-06 16:08:14 -08:00
|
|
|
|
|
2020-10-05 11:55:15 +01:00
|
|
|
|
if step_index == 0:
|
2021-07-21 16:06:23 +01:00
|
|
|
|
if should_skip_template_page(template._template):
|
2025-04-25 10:23:28 -04:00
|
|
|
|
return {
|
|
|
|
|
|
"href": {
|
2025-04-25 16:20:21 -04:00
|
|
|
|
"url": url_for(
|
|
|
|
|
|
".choose_template",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
),
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"text": "Back to all templates",
|
2025-04-25 10:23:28 -04:00
|
|
|
|
},
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"html": "Back to all templates",
|
2025-04-25 10:23:28 -04:00
|
|
|
|
}
|
2018-06-13 14:32:52 +01:00
|
|
|
|
else:
|
2025-04-25 10:23:28 -04:00
|
|
|
|
return {
|
|
|
|
|
|
"href": {
|
2025-04-25 16:20:21 -04:00
|
|
|
|
"url": url_for(
|
|
|
|
|
|
".view_template",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
template_id=template.id,
|
|
|
|
|
|
),
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"text": "Back to confirm your template",
|
2025-04-25 10:23:28 -04:00
|
|
|
|
},
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"html": "Back to confirm your template",
|
2025-04-25 10:23:28 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-25 16:20:21 -04:00
|
|
|
|
# fallback for other steps
|
2025-04-25 10:23:28 -04:00
|
|
|
|
back_to_text = (
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"Back to select recipients"
|
|
|
|
|
|
if step_index == 1
|
|
|
|
|
|
else "Back to message personalization"
|
2025-04-25 10:23:28 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"href": {
|
|
|
|
|
|
"url": url_for(
|
|
|
|
|
|
"main.send_one_off_step",
|
2018-06-13 14:32:52 +01:00
|
|
|
|
service_id=service_id,
|
2018-08-09 16:29:51 +01:00
|
|
|
|
template_id=template.id,
|
2025-04-25 10:23:28 -04:00
|
|
|
|
step_index=step_index - 1,
|
|
|
|
|
|
),
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"text": back_to_text,
|
2025-04-25 10:23:28 -04:00
|
|
|
|
},
|
2025-05-14 10:08:02 -07:00
|
|
|
|
"html": back_to_text,
|
2025-04-25 10:23:28 -04:00
|
|
|
|
}
|
2017-06-23 15:02:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
2020-04-09 16:37:38 +01:00
|
|
|
|
def get_skip_link(step_index, template):
|
|
|
|
|
|
if (
|
2023-08-25 09:12:23 -07:00
|
|
|
|
request.endpoint == "main.send_one_off_step"
|
2020-04-09 16:37:38 +01:00
|
|
|
|
and step_index == 0
|
2020-10-05 16:16:11 +01:00
|
|
|
|
and template.template_type in ("sms", "email")
|
2023-08-25 09:12:23 -07:00
|
|
|
|
and not (template.template_type == "sms" and current_user.mobile_number is None)
|
|
|
|
|
|
and current_user.has_permissions("manage_templates", "manage_service")
|
2020-04-09 16:37:38 +01:00
|
|
|
|
):
|
|
|
|
|
|
return (
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"Use my {}".format(first_column_headings[template.template_type][0]),
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
".send_one_off_to_myself",
|
|
|
|
|
|
service_id=current_service.id,
|
|
|
|
|
|
template_id=template.id,
|
|
|
|
|
|
),
|
2020-04-09 16:37:38 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@main.route(
|
|
|
|
|
|
"/services/<uuid:service_id>/template/<uuid:template_id>/one-off/send-to-myself",
|
|
|
|
|
|
methods=["GET"],
|
|
|
|
|
|
)
|
|
|
|
|
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
2020-10-05 16:16:11 +01:00
|
|
|
|
def send_one_off_to_myself(service_id, template_id):
|
2024-09-26 08:23:13 -07:00
|
|
|
|
current_app.logger.info("Send one off to myself")
|
|
|
|
|
|
try:
|
|
|
|
|
|
db_template = current_service.get_template_with_user_permission_or_403(
|
|
|
|
|
|
template_id, current_user
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
current_app.logger.exception("Couldnt get template for one off")
|
|
|
|
|
|
# Use 406 just because we're limited to certain codes here and it will point us back to a problem here
|
|
|
|
|
|
abort(406)
|
2020-10-05 16:16:11 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if db_template["template_type"] not in ("sms", "email"):
|
2020-10-05 16:16:11 +01:00
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
|
|
|
|
# We aren't concerned with creating the exact template (for example adding recipient and sender names)
|
|
|
|
|
|
# we just want to create enough to use `fields_to_fill_in`
|
|
|
|
|
|
template = get_template(
|
|
|
|
|
|
db_template,
|
|
|
|
|
|
current_service,
|
|
|
|
|
|
)
|
|
|
|
|
|
fields_to_fill_in(template, prefill_current_user=True)
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
"main.send_one_off_step",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
template_id=template_id,
|
|
|
|
|
|
step_index=1,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2020-10-05 16:16:11 +01:00
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@main.route(
|
|
|
|
|
|
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
|
|
|
|
|
|
methods=["GET"],
|
|
|
|
|
|
)
|
|
|
|
|
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
2017-06-23 15:02:20 +01:00
|
|
|
|
def check_notification(service_id, template_id):
|
2018-10-30 14:24:50 +00:00
|
|
|
|
return render_template(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"views/notifications/check.html",
|
2024-04-01 18:03:38 -07:00
|
|
|
|
**_check_notification(service_id, template_id, show_recipient=True),
|
2018-10-30 14:24:50 +00:00
|
|
|
|
)
|
2017-06-29 15:31:44 +01:00
|
|
|
|
|
|
|
|
|
|
|
2024-04-01 18:03:38 -07:00
|
|
|
|
def _check_notification(service_id, template_id, exception=None, **kwargs):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
db_template = current_service.get_template_with_user_permission_or_403(
|
|
|
|
|
|
template_id, current_user
|
|
|
|
|
|
)
|
2017-11-02 12:07:46 +00:00
|
|
|
|
email_reply_to = None
|
|
|
|
|
|
sms_sender = None
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if db_template["template_type"] == "email":
|
2018-10-26 17:31:49 +01:00
|
|
|
|
email_reply_to = get_email_reply_to_address_from_session()
|
2023-08-25 09:12:23 -07:00
|
|
|
|
elif db_template["template_type"] == "sms":
|
2018-10-26 17:31:49 +01:00
|
|
|
|
sms_sender = get_sms_sender_from_session()
|
2017-06-23 15:02:20 +01:00
|
|
|
|
template = get_template(
|
|
|
|
|
|
db_template,
|
|
|
|
|
|
current_service,
|
2017-11-02 12:07:46 +00:00
|
|
|
|
email_reply_to=email_reply_to,
|
2018-10-30 14:24:50 +00:00
|
|
|
|
sms_sender=sms_sender,
|
2024-04-01 18:03:38 -07:00
|
|
|
|
**kwargs,
|
2024-02-16 17:19:03 -08:00
|
|
|
|
)
|
2020-04-15 15:42:25 +01:00
|
|
|
|
placeholders = fields_to_fill_in(template)
|
|
|
|
|
|
|
|
|
|
|
|
back_link = get_back_link(service_id, template, len(placeholders), placeholders)
|
2017-06-26 17:09:51 +01:00
|
|
|
|
|
2024-02-08 18:43:17 -08:00
|
|
|
|
back_link_from_preview = get_back_link(
|
|
|
|
|
|
service_id, template, len(placeholders), placeholders, preview=True
|
|
|
|
|
|
)
|
2024-02-06 16:08:14 -08:00
|
|
|
|
|
2024-01-16 15:37:56 -08:00
|
|
|
|
choose_time_form = ChooseTimeForm()
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if (not session.get("recipient")) or not all_placeholders_in_session(
|
|
|
|
|
|
template.placeholders
|
2017-06-26 17:09:51 +01:00
|
|
|
|
):
|
2025-04-28 11:23:00 -04:00
|
|
|
|
raise PermanentRedirect(back_link["href"]["url"])
|
2017-06-26 17:09:51 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
template.values = get_recipient_and_placeholders_from_session(
|
|
|
|
|
|
template.template_type
|
|
|
|
|
|
)
|
2018-10-30 14:24:50 +00:00
|
|
|
|
return dict(
|
2017-06-23 15:02:20 +01:00
|
|
|
|
template=template,
|
|
|
|
|
|
back_link=back_link,
|
2024-02-06 16:08:14 -08:00
|
|
|
|
back_link_from_preview=back_link_from_preview,
|
2024-01-16 15:37:56 -08:00
|
|
|
|
choose_time_form=choose_time_form,
|
2018-10-30 14:24:50 +00:00
|
|
|
|
**(get_template_error_dict(exception) if exception else {}),
|
2017-06-23 15:02:20 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-06-29 15:31:44 +01:00
|
|
|
|
def get_template_error_dict(exception):
|
2017-06-29 16:06:36 +01:00
|
|
|
|
# TODO: Make API return some computer-friendly identifier as well as the end user error messages
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if "service is in trial mode" in exception.message:
|
|
|
|
|
|
error = "not-allowed-to-send-to"
|
|
|
|
|
|
elif "Exceeded send limits" in exception.message:
|
|
|
|
|
|
error = "too-many-messages"
|
2020-03-09 17:03:22 +00:00
|
|
|
|
# the error from the api is changing for message-too-long, but we need both until the api is deployed.
|
2023-08-25 09:12:23 -07:00
|
|
|
|
elif (
|
|
|
|
|
|
"Content for template has a character count greater than the limit of"
|
|
|
|
|
|
in exception.message
|
|
|
|
|
|
):
|
|
|
|
|
|
error = "message-too-long"
|
|
|
|
|
|
elif "Text messages cannot be longer than" in exception.message:
|
|
|
|
|
|
error = "message-too-long"
|
2017-06-29 15:31:44 +01:00
|
|
|
|
else:
|
|
|
|
|
|
raise exception
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"error": error,
|
|
|
|
|
|
"SMS_CHAR_COUNT_LIMIT": SMS_CHAR_COUNT_LIMIT,
|
|
|
|
|
|
"current_service": current_service,
|
2017-06-29 16:06:36 +01:00
|
|
|
|
# used to trigger CSV specific err msg content, so not needed for single notification errors.
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"original_file_name": False,
|
2017-06-29 15:31:44 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-02-06 16:08:14 -08:00
|
|
|
|
@main.route(
|
|
|
|
|
|
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check/preview",
|
2024-03-08 15:05:44 -08:00
|
|
|
|
methods=["POST"],
|
2024-02-06 16:08:14 -08:00
|
|
|
|
)
|
|
|
|
|
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
|
|
|
|
|
def preview_notification(service_id, template_id):
|
|
|
|
|
|
recipient = get_recipient()
|
|
|
|
|
|
if not recipient:
|
2025-03-13 15:04:12 -07:00
|
|
|
|
current_app.logger.warning(
|
|
|
|
|
|
f"No recipient found for service {service_id}, template {template_id}. Redirecting..."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2024-02-06 16:08:14 -08:00
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
".send_one_off",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
template_id=template_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2024-03-08 15:05:44 -08:00
|
|
|
|
session["scheduled_for"] = request.form.get("scheduled_for", "")
|
2024-02-06 16:08:14 -08:00
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
|
"views/notifications/preview.html",
|
2024-04-09 12:52:45 -07:00
|
|
|
|
**_check_notification(
|
|
|
|
|
|
service_id, template_id, show_recipient=False, force_hide_sender=True
|
|
|
|
|
|
),
|
2024-02-08 18:43:17 -08:00
|
|
|
|
scheduled_for=session["scheduled_for"],
|
2024-02-16 11:44:25 -08:00
|
|
|
|
recipient=recipient,
|
2024-02-06 16:08:14 -08:00
|
|
|
|
)
|
|
|
|
|
|
|
2017-06-29 15:31:44 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
@main.route(
|
|
|
|
|
|
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
|
|
|
|
|
|
methods=["POST"],
|
|
|
|
|
|
)
|
|
|
|
|
|
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
2017-06-23 15:02:20 +01:00
|
|
|
|
def send_notification(service_id, template_id):
|
2024-09-02 09:57:22 -07:00
|
|
|
|
recipient = get_recipient()
|
|
|
|
|
|
|
|
|
|
|
|
if not recipient:
|
|
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
".send_one_off",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
template_id=template_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2024-09-02 07:51:57 -07:00
|
|
|
|
upload_id = _send_notification(service_id, template_id)
|
|
|
|
|
|
|
2024-09-02 09:17:51 -07:00
|
|
|
|
session.pop("recipient", "")
|
|
|
|
|
|
session.pop("placeholders", "")
|
2024-09-02 07:51:57 -07:00
|
|
|
|
|
|
|
|
|
|
# We have to wait for the job to run and create the notification in the database
|
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
|
notifications = notification_api_client.get_notifications_for_service(
|
|
|
|
|
|
service_id, job_id=upload_id, include_one_off=True
|
|
|
|
|
|
)
|
|
|
|
|
|
attempts = 0
|
|
|
|
|
|
|
|
|
|
|
|
# The response can come back in different forms of incompleteness
|
|
|
|
|
|
while (
|
|
|
|
|
|
notifications["total"] == 0
|
|
|
|
|
|
and notifications["notifications"] == []
|
|
|
|
|
|
and attempts < 50
|
|
|
|
|
|
):
|
|
|
|
|
|
notifications = notification_api_client.get_notifications_for_service(
|
|
|
|
|
|
service_id, job_id=upload_id, include_one_off=True
|
|
|
|
|
|
)
|
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
|
attempts = attempts + 1
|
|
|
|
|
|
|
|
|
|
|
|
if notifications["total"] == 0 and attempts == 50:
|
|
|
|
|
|
# This shows the job we auto-generated for the user
|
|
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
"main.view_job",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
job_id=upload_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
total = notifications["total"]
|
|
|
|
|
|
current_app.logger.info(
|
|
|
|
|
|
hilite(
|
|
|
|
|
|
f"job_id: {upload_id} has notifications: {total} and attempts: {attempts}"
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return redirect(
|
|
|
|
|
|
url_for(
|
|
|
|
|
|
".view_job",
|
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
|
job_id=upload_id,
|
|
|
|
|
|
# used to show the final step of the tour (help=3) or not show
|
|
|
|
|
|
# a back link on a just sent one off notification (help=0)
|
|
|
|
|
|
help=request.args.get("help"),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2024-08-30 09:50:01 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _send_notification(service_id, template_id):
|
2024-02-08 18:43:17 -08:00
|
|
|
|
scheduled_for = session.pop("scheduled_for", "")
|
2024-09-02 07:51:57 -07:00
|
|
|
|
|
2023-12-14 13:24:34 -08:00
|
|
|
|
keys = []
|
|
|
|
|
|
values = []
|
2024-09-24 10:35:22 -07:00
|
|
|
|
# Guarantee that the real phone number comes last, because some
|
|
|
|
|
|
# users will have placeholders like "add your second phone number"
|
|
|
|
|
|
# or something like as custom placeholders.
|
2023-12-14 13:24:34 -08:00
|
|
|
|
for k, v in session["placeholders"].items():
|
2024-09-24 10:35:22 -07:00
|
|
|
|
if k != "phone number":
|
|
|
|
|
|
keys.append(k)
|
|
|
|
|
|
values.append(v)
|
2024-09-27 07:20:40 -07:00
|
|
|
|
if "phone number" in session["placeholders"].keys():
|
|
|
|
|
|
keys.append("phone number")
|
|
|
|
|
|
values.append(session["placeholders"]["phone number"])
|
2023-12-14 13:24:34 -08:00
|
|
|
|
|
|
|
|
|
|
data = ",".join(keys)
|
|
|
|
|
|
vals = ",".join(values)
|
|
|
|
|
|
data = f"{data}\r\n{vals}"
|
2024-05-02 12:53:27 -06:00
|
|
|
|
filename = (
|
|
|
|
|
|
f"one-off-{uuid.uuid4()}.csv" # {current_user.name} removed from filename
|
|
|
|
|
|
)
|
2023-12-18 12:31:40 -08:00
|
|
|
|
my_data = {"filename": filename, "template_id": template_id, "data": data}
|
2023-12-14 13:24:34 -08:00
|
|
|
|
upload_id = s3upload(service_id, my_data)
|
2024-06-17 11:13:03 -07:00
|
|
|
|
# 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.
|
2024-07-01 10:53:21 -07:00
|
|
|
|
current_app.logger.info(
|
|
|
|
|
|
hilite(
|
2024-09-27 08:39:19 -07:00
|
|
|
|
f"One-off file: {filename} job_id: {upload_id} s3 location: {service_id}-service-notify/{upload_id}.csv"
|
2024-07-01 10:53:21 -07:00
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2024-09-03 10:37:05 -07:00
|
|
|
|
# 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)
|
2024-09-02 07:51:57 -07:00
|
|
|
|
|
2023-12-18 12:31:40 -08:00
|
|
|
|
job_api_client.create_job(
|
|
|
|
|
|
upload_id,
|
|
|
|
|
|
service_id,
|
2024-02-06 16:08:14 -08:00
|
|
|
|
scheduled_for=scheduled_for,
|
2023-12-18 12:31:40 -08:00
|
|
|
|
template_id=template_id,
|
|
|
|
|
|
original_file_name=filename,
|
|
|
|
|
|
notification_count=1,
|
|
|
|
|
|
valid="True",
|
|
|
|
|
|
)
|
2024-09-02 07:51:57 -07:00
|
|
|
|
return upload_id
|
2017-10-17 16:06:15 +01:00
|
|
|
|
|
|
|
|
|
|
|
2018-10-26 17:31:49 +01:00
|
|
|
|
def get_email_reply_to_address_from_session():
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if session.get("sender_id"):
|
|
|
|
|
|
return current_service.get_email_reply_to_address(session["sender_id"])[
|
|
|
|
|
|
"email_address"
|
|
|
|
|
|
]
|
2017-11-02 12:07:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
2018-10-26 17:31:49 +01:00
|
|
|
|
def get_sms_sender_from_session():
|
2024-07-22 16:34:21 -04:00
|
|
|
|
sender_id = session.get("sender_id")
|
|
|
|
|
|
if sender_id:
|
|
|
|
|
|
sms_sender = current_service.get_sms_sender(session["sender_id"])["sms_sender"]
|
|
|
|
|
|
current_app.logger.info(f"SMS Sender ({sender_id}) #: {sms_sender}")
|
|
|
|
|
|
return sms_sender
|
|
|
|
|
|
else:
|
|
|
|
|
|
current_app.logger.error("No SMS Sender!!!!!!")
|
2018-06-08 12:49:29 +01:00
|
|
|
|
|
2024-07-22 17:05:44 -04:00
|
|
|
|
|
2018-06-08 12:49:29 +01:00
|
|
|
|
def get_spreadsheet_column_headings_from_template(template):
|
|
|
|
|
|
column_headings = []
|
|
|
|
|
|
|
2022-12-05 15:33:44 -05:00
|
|
|
|
recipient_columns = first_column_headings[template.template_type]
|
2020-04-27 10:57:25 +01:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
for column_heading in recipient_columns + list(template.placeholders):
|
2022-02-04 10:43:36 +00:00
|
|
|
|
if column_heading not in InsensitiveDict.from_keys(column_headings):
|
2018-06-08 12:49:29 +01:00
|
|
|
|
column_headings.append(column_heading)
|
|
|
|
|
|
|
|
|
|
|
|
return column_headings
|
Fix 500 error due to inconsistent recipient check
This strengthens the initial check of what's in the session to make
sure it contains some kind of recipient. Without this, we get:
Traceback (most recent call last):
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/flask/app.py", line 1950, in full_dispatch_request
rv = self.dispatch_request()
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/flask/app.py", line 1936, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/home/vcap/app/app/utils/user.py", line 26, in wrap_func
return func(*args, **kwargs)
File "/home/vcap/app/app/main/views/send.py", line 1041, in send_notification
recipient=session['recipient'] or InsensitiveDict(session['placeholders'])['address line 1'],
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/notifications_utils/insensitive_dict.py", line 41, in __getitem__
return super().__getitem__(self.make_key(key))
KeyError: 'addressline1'
I'm not sure how to reproduce this, but this should at least give
the user a better experience, instead of a 500 page.
2022-02-18 12:38:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_recipient():
|
2024-08-26 09:02:39 -07:00
|
|
|
|
set_timezone()
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if {"recipient", "placeholders"} - set(session.keys()):
|
Fix 500 error due to inconsistent recipient check
This strengthens the initial check of what's in the session to make
sure it contains some kind of recipient. Without this, we get:
Traceback (most recent call last):
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/flask/app.py", line 1950, in full_dispatch_request
rv = self.dispatch_request()
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/flask/app.py", line 1936, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/home/vcap/app/app/utils/user.py", line 26, in wrap_func
return func(*args, **kwargs)
File "/home/vcap/app/app/main/views/send.py", line 1041, in send_notification
recipient=session['recipient'] or InsensitiveDict(session['placeholders'])['address line 1'],
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/notifications_utils/insensitive_dict.py", line 41, in __getitem__
return super().__getitem__(self.make_key(key))
KeyError: 'addressline1'
I'm not sure how to reproduce this, but this should at least give
the user a better experience, instead of a 500 page.
2022-02-18 12:38:02 +00:00
|
|
|
|
return None
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
return session["recipient"] or InsensitiveDict(session["placeholders"]).get(
|
|
|
|
|
|
"address line 1"
|
Fix 500 error due to inconsistent recipient check
This strengthens the initial check of what's in the session to make
sure it contains some kind of recipient. Without this, we get:
Traceback (most recent call last):
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/flask/app.py", line 1950, in full_dispatch_request
rv = self.dispatch_request()
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/flask/app.py", line 1936, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/home/vcap/app/app/utils/user.py", line 26, in wrap_func
return func(*args, **kwargs)
File "/home/vcap/app/app/main/views/send.py", line 1041, in send_notification
recipient=session['recipient'] or InsensitiveDict(session['placeholders'])['address line 1'],
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/notifications_utils/insensitive_dict.py", line 41, in __getitem__
return super().__getitem__(self.make_key(key))
KeyError: 'addressline1'
I'm not sure how to reproduce this, but this should at least give
the user a better experience, instead of a 500 page.
2022-02-18 12:38:02 +00:00
|
|
|
|
)
|