Merge branch 'main' of https://github.com/GSA/notifications-admin into 1043-move-account-info-into-utility-nav

This commit is contained in:
Jonathan Bobel
2024-01-01 10:35:05 -05:00
49 changed files with 412 additions and 3212 deletions

View File

@@ -110,11 +110,7 @@ from app.notify_client.template_folder_api_client import template_folder_api_cli
from app.notify_client.template_statistics_api_client import template_statistics_client
from app.notify_client.upload_api_client import upload_api_client
from app.notify_client.user_api_client import user_api_client
from app.url_converters import (
SimpleDateTypeConverter,
TemplateTypeConverter,
TicketTypeConverter,
)
from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter
login_manager = LoginManager()
csrf = CSRFProtect()
@@ -326,7 +322,6 @@ def init_app(application):
application.url_map.converters["uuid"].to_python = lambda self, value: value
application.url_map.converters["template_type"] = TemplateTypeConverter
application.url_map.converters["ticket_type"] = TicketTypeConverter
application.url_map.converters["simple_date"] = SimpleDateTypeConverter

View File

@@ -479,3 +479,7 @@ details form {
padding-left: 5px;
letter-spacing: 0.04em;
}
.edit-textbox-error-mt {
margin-top: 1.5rem;
}

View File

@@ -24,6 +24,7 @@ EVENT_SCHEMAS = {
"service_id",
"ui_permissions",
},
"resend_user_invite_to_service": {"email_address", "resent_by_id", "service_id"},
"cancel_user_invite_to_service": {"email_address", "canceled_by_id", "service_id"},
"set_user_permissions": {
"user_id",
@@ -63,6 +64,10 @@ def create_cancel_user_invite_to_service_event(**kwargs):
_send_event("cancel_user_invite_to_service", **kwargs)
def create_resend_user_invite_to_service_event(**kwargs):
_send_event("resend_user_invite_to_service", **kwargs)
def create_add_user_to_service_event(**kwargs):
_send_event("add_user_to_service", **kwargs)

View File

@@ -61,7 +61,6 @@ from app.main.validators import (
ValidEmail,
ValidGovEmail,
)
from app.models.feedback import PROBLEM_TICKET_TYPE, QUESTION_TICKET_TYPE
from app.models.organization import Organization
from app.utils import merge_jsonlike
from app.utils.csv import get_user_preferred_timezone
@@ -1319,49 +1318,6 @@ class CreateKeyForm(StripWhitespaceForm):
raise ValidationError("A key with this name already exists")
class SupportType(StripWhitespaceForm):
support_type = GovukRadiosField(
"How can we help you?",
choices=[
(PROBLEM_TICKET_TYPE, "Report a problem"),
(QUESTION_TICKET_TYPE, "Ask a question or give feedback"),
],
)
class SupportRedirect(StripWhitespaceForm):
who = GovukRadiosField(
"What do you need help with?",
choices=[
(
"public-sector",
"I work in the public sector and need to send emails or text messages",
),
("public", "Im a member of the public with a question for the government"),
],
param_extensions={"fieldset": {"legend": {"classes": "usa-sr-only"}}},
)
class FeedbackOrProblem(StripWhitespaceForm):
name = GovukTextInputField("Name (optional)")
email_address = email_address(label="Email address", gov_user=False, required=True)
feedback = TextAreaField(
"Your message", validators=[DataRequired(message="Cannot be empty")]
)
class Triage(StripWhitespaceForm):
severe = GovukRadiosField(
"Is it an emergency?",
choices=[
("yes", "Yes"),
("no", "No"),
],
thing="yes or no",
)
class EstimateUsageForm(StripWhitespaceForm):
volume_email = ForgivingIntegerField(
"How many emails do you expect to send in the next year?",
@@ -1905,13 +1861,6 @@ class AdminClearCacheForm(StripWhitespaceForm):
raise ValidationError("Select at least one option")
class AdminOrganizationGoLiveNotesForm(StripWhitespaceForm):
request_to_go_live_notes = TextAreaField(
"Go live notes",
filters=[lambda x: x or None],
)
class ChangeSecurityKeyNameForm(StripWhitespaceForm):
security_key_name = GovukTextInputField(
"Name of key",

View File

@@ -1,239 +1,10 @@
from datetime import datetime
from flask import render_template
import pytz
from flask import redirect, render_template, request, session, url_for
from flask_login import current_user
from govuk_bank_holidays.bank_holidays import BankHolidays
from notifications_utils.clients.zendesk.zendesk_client import NotifySupportTicket
from app import convert_to_boolean, current_service
from app.extensions import zendesk_client
from app.main import main
from app.main.forms import FeedbackOrProblem, SupportRedirect, SupportType, Triage
from app.models.feedback import (
GENERAL_TICKET_TYPE,
PROBLEM_TICKET_TYPE,
QUESTION_TICKET_TYPE,
)
from app.utils import hide_from_search_engines
bank_holidays = BankHolidays(use_cached_holidays=True)
from app.utils.user import user_is_logged_in
@main.route("/support", methods=["GET", "POST"])
@hide_from_search_engines
@main.route("/support", methods=["GET"])
@user_is_logged_in
def support():
if current_user.is_authenticated:
form = SupportType()
if form.validate_on_submit():
return redirect(
url_for(
".feedback",
ticket_type=form.support_type.data,
)
)
else:
form = SupportRedirect()
if form.validate_on_submit():
if form.who.data == "public":
return redirect(url_for(".support_public"))
else:
return redirect(
url_for(
".feedback",
ticket_type=GENERAL_TICKET_TYPE,
)
)
return render_template("views/support/index.html", form=form)
@main.route("/support/public")
@hide_from_search_engines
def support_public():
return render_template("views/support/public.html")
@main.route("/support/triage", methods=["GET", "POST"])
@main.route("/support/triage/<ticket_type:ticket_type>", methods=["GET", "POST"])
@hide_from_search_engines
def triage(ticket_type=PROBLEM_TICKET_TYPE):
form = Triage()
if form.validate_on_submit():
return redirect(
url_for(".feedback", ticket_type=ticket_type, severe=form.severe.data)
)
return render_template(
"views/support/triage.html",
form=form,
page_title={
PROBLEM_TICKET_TYPE: "Report a problem",
GENERAL_TICKET_TYPE: "Contact Notify.gov support",
}.get(ticket_type),
)
@main.route("/support/<ticket_type:ticket_type>", methods=["GET", "POST"])
@hide_from_search_engines
def feedback(ticket_type):
form = FeedbackOrProblem()
if not form.feedback.data:
form.feedback.data = session.pop("feedback_message", "")
if request.args.get("severe") in ["yes", "no"]:
severe = convert_to_boolean(request.args.get("severe"))
else:
severe = None
out_of_hours_emergency = all(
(
ticket_type != QUESTION_TICKET_TYPE,
not in_business_hours(),
severe,
)
)
if needs_triage(ticket_type, severe):
session["feedback_message"] = form.feedback.data
return redirect(url_for(".triage", ticket_type=ticket_type))
if needs_escalation(ticket_type, severe):
return redirect(url_for(".bat_phone"))
if current_user.is_authenticated:
form.email_address.data = current_user.email_address
form.name.data = current_user.name
if form.validate_on_submit():
user_email = form.email_address.data
user_name = form.name.data or None
feedback_msg = render_template(
"support-tickets/support-ticket.txt",
content=form.feedback.data,
)
ticket = NotifySupportTicket(
subject="Notify feedback",
message=feedback_msg,
ticket_type=get_zendesk_ticket_type(ticket_type),
p1=out_of_hours_emergency,
user_name=user_name,
user_email=user_email,
org_id=current_service.organization_id if current_service else None,
org_type=current_service.organization_type if current_service else None,
service_id=current_service.id if current_service else None,
)
zendesk_client.send_ticket_to_zendesk(ticket)
return redirect(
url_for(
".thanks",
out_of_hours_emergency=out_of_hours_emergency,
email_address_provided=(
current_user.is_authenticated or bool(form.email_address.data)
),
)
)
return render_template(
"views/support/form.html",
form=form,
back_link=(
url_for(".support")
if severe is None
else url_for(".triage", ticket_type=ticket_type)
),
show_status_page_banner=(ticket_type == PROBLEM_TICKET_TYPE),
page_title={
GENERAL_TICKET_TYPE: "Contact Notify.gov support",
PROBLEM_TICKET_TYPE: "Report a problem",
QUESTION_TICKET_TYPE: "Ask a question or give feedback",
}.get(ticket_type),
)
@main.route("/support/escalate", methods=["GET", "POST"])
@hide_from_search_engines
def bat_phone():
if current_user.is_authenticated:
return redirect(url_for("main.feedback", ticket_type=PROBLEM_TICKET_TYPE))
return render_template("views/support/bat-phone.html")
@main.route("/support/thanks", methods=["GET", "POST"])
@hide_from_search_engines
def thanks():
return render_template(
"views/support/thanks.html",
out_of_hours_emergency=convert_to_boolean(
request.args.get("out_of_hours_emergency")
),
email_address_provided=convert_to_boolean(
request.args.get("email_address_provided")
),
out_of_hours=not in_business_hours(),
)
def in_business_hours():
now = datetime.utcnow().replace(tzinfo=pytz.utc)
if is_weekend(now) or is_bank_holiday(now):
return False
return london_time_today_as_utc(9, 30) <= now < london_time_today_as_utc(17, 30)
def london_time_today_as_utc(hour, minute):
return (
pytz.timezone("Europe/London")
.localize(datetime.now().replace(hour=hour, minute=minute))
.astimezone(pytz.utc)
)
def is_weekend(time):
return time.strftime("%A") in {
"Saturday",
"Sunday",
}
def is_bank_holiday(time):
return bank_holidays.is_holiday(time.date())
def needs_triage(ticket_type, severe):
return all(
(
ticket_type != QUESTION_TICKET_TYPE,
severe is None,
(not current_user.is_authenticated or current_user.live_services),
not in_business_hours(),
)
)
def needs_escalation(ticket_type, severe):
return all(
(
ticket_type != QUESTION_TICKET_TYPE,
severe,
not current_user.is_authenticated,
not in_business_hours(),
)
)
def get_zendesk_ticket_type(ticket_type):
# Zendesk has 4 ticket types - "problem", "incident", "task" and "question".
# We don't want to use a Zendesk "problem" ticket type when someone reports a
# Notify problem because they are designed to group multiple incident tickets together,
# allowing them to be solved as a group.
if ticket_type == PROBLEM_TICKET_TYPE:
return NotifySupportTicket.TYPE_INCIDENT
return NotifySupportTicket.TYPE_QUESTION
return render_template("views/support/index.html")

View File

@@ -9,6 +9,7 @@ from app.event_handlers import (
create_invite_user_to_service_event,
create_mobile_number_change_event,
create_remove_user_from_service_event,
create_resend_user_invite_to_service_event,
)
from app.formatters import redact_mobile_number
from app.main import main
@@ -331,3 +332,22 @@ def cancel_invited_user(service_id, invited_user_id):
flash(f"Invitation cancelled for {invited_user.email_address}", "default_with_tick")
return redirect(url_for("main.manage_users", service_id=service_id))
@main.route(
"/services/<uuid:service_id>/resend-invite/<uuid:invited_user_id>",
methods=["GET"],
)
@user_has_permissions("manage_service")
def resend_invite(service_id, invited_user_id):
current_service.resend_invite(invited_user_id)
invited_user = InvitedUser.by_id_and_service_id(service_id, invited_user_id)
create_resend_user_invite_to_service_event(
email_address=invited_user.email_address,
resent_by_id=current_user.id,
service_id=service_id,
)
flash(f"Invitation resent for {invited_user.email_address}", "default_with_tick")
return redirect(url_for("main.manage_users", service_id=service_id))

View File

@@ -3,6 +3,7 @@ from datetime import datetime
from flask import (
Response,
flash,
jsonify,
render_template,
request,
@@ -32,14 +33,16 @@ from app.utils.user import user_has_permissions
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>")
@user_has_permissions("view_activity", "send_messages")
def view_notification(service_id, notification_id):
def view_notification(service_id, notification_id, error_message=None):
if error_message:
flash(error_message)
notification = notification_api_client.get_notification(
service_id, str(notification_id)
)
notification["template"].update({"reply_to_text": notification["reply_to_text"]})
personalisation = get_all_personalisation_from_notification(notification)
error_message = None
template = get_template(
notification["template"],

View File

@@ -13,7 +13,6 @@ from app.main.forms import (
AdminNewOrganizationForm,
AdminNotesForm,
AdminOrganizationDomainsForm,
AdminOrganizationGoLiveNotesForm,
InviteOrgUserForm,
OrganizationOrganizationTypeForm,
RenameOrganizationForm,
@@ -313,28 +312,6 @@ def edit_organization_domains(org_id):
)
@main.route(
"/organizations/<uuid:org_id>/settings/edit-go-live-notes", methods=["GET", "POST"]
)
@user_is_platform_admin
def edit_organization_go_live_notes(org_id):
form = AdminOrganizationGoLiveNotesForm()
if form.validate_on_submit():
organizations_client.update_organization(
org_id, request_to_go_live_notes=form.request_to_go_live_notes.data
)
return redirect(url_for(".organization_settings", org_id=org_id))
org = organizations_client.get_organization(org_id)
form.request_to_go_live_notes.data = org["request_to_go_live_notes"]
return render_template(
"views/organizations/organization/settings/edit-go-live-notes.html",
form=form,
)
@main.route("/organizations/<uuid:org_id>/settings/notes", methods=["GET", "POST"])
@user_is_platform_admin
def edit_organization_notes(org_id):

View File

@@ -1,17 +1,10 @@
import itertools
import time
import uuid
from string import ascii_uppercase
from zipfile import BadZipFile
from flask import (
abort,
current_app,
flash,
redirect,
render_template,
request,
session,
url_for,
)
from flask import abort, flash, redirect, render_template, request, session, url_for
from flask_login import current_user
from notifications_python_client.errors import HTTPError
from notifications_utils import SMS_CHAR_COUNT_LIMIT
@@ -495,7 +488,6 @@ def _check_messages(service_id, template_id, upload_id, preview_row):
remaining_messages = current_service.message_limit - notification_count
contents = s3download(service_id, upload_id)
db_template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
)
@@ -836,7 +828,6 @@ def get_template_error_dict(exception):
@user_has_permissions("send_messages", restrict_admin_usage=True)
def send_notification(service_id, template_id):
recipient = get_recipient()
if not recipient:
return redirect(
url_for(
@@ -846,38 +837,69 @@ def send_notification(service_id, template_id):
)
)
db_template = current_service.get_template_with_user_permission_or_403(
template_id, current_user
keys = []
values = []
for k, v in session["placeholders"].items():
keys.append(k)
values.append(v)
data = ",".join(keys)
vals = ",".join(values)
data = f"{data}\r\n{vals}"
filename = f"one-off-{current_user.name}-{uuid.uuid4()}.csv"
my_data = {"filename": filename, "template_id": template_id, "data": data}
upload_id = s3upload(service_id, my_data)
form = CsvUploadForm()
form.file.data = my_data
form.file.name = filename
check_message_output = check_messages(service_id, template_id, upload_id, 2)
if "You cannot send to" in check_message_output:
return check_messages(service_id, template_id, upload_id, 2)
job_api_client.create_job(
upload_id,
service_id,
scheduled_for="",
template_id=template_id,
original_file_name=filename,
notification_count=1,
valid="True",
)
try:
noti = notification_api_client.send_notification(
service_id,
template_id=db_template["id"],
recipient=recipient,
personalisation=session["placeholders"],
sender_id=session.get("sender_id", None),
session.pop("recipient")
session.pop("placeholders")
# We have to wait for the job to run and create the notification in the database
time.sleep(0.1)
notifications = notification_api_client.get_notifications_for_service(
service_id, job_id=upload_id, include_one_off=True
)
attempts = 0
while notifications["total"] == 0 and attempts < 5:
notifications = notification_api_client.get_notifications_for_service(
service_id, job_id=upload_id, include_one_off=True
)
except HTTPError as exception:
current_app.logger.error(
'Service {} could not send notification: "{}"'.format(
current_service.id, exception.message
time.sleep(0.1)
attempts = attempts + 1
if notifications["total"] == 0 and attempts == 5:
# 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,
)
)
return render_template(
"views/notifications/check.html",
**_check_notification(service_id, template_id, exception),
)
session.pop("placeholders")
session.pop("recipient")
session.pop("sender_id", None)
return redirect(
url_for(
".view_notification",
service_id=service_id,
notification_id=noti["id"],
from_job=upload_id,
notification_id=notifications["notifications"][0]["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"),

View File

@@ -13,7 +13,6 @@ from flask import (
)
from flask_login import current_user
from notifications_python_client.errors import HTTPError
from notifications_utils.clients.zendesk.zendesk_client import NotifySupportTicket
from app import (
billing_api_client,
@@ -28,7 +27,6 @@ from app.event_handlers import (
create_resume_service_event,
create_suspend_service_event,
)
from app.extensions import zendesk_client
from app.formatters import email_safe
from app.main import main
from app.main.forms import (
@@ -41,7 +39,6 @@ from app.main.forms import (
AdminServiceRateLimitForm,
AdminServiceSMSAllowanceForm,
AdminSetOrganizationForm,
EstimateUsageForm,
RenameServiceForm,
SearchByNameForm,
ServiceContactDetailsForm,
@@ -54,11 +51,7 @@ from app.main.forms import (
)
from app.utils import DELIVERED_STATUSES, FAILURE_STATUSES, SENDING_STATUSES
from app.utils.time import parse_naive_dt
from app.utils.user import (
user_has_permissions,
user_is_gov_user,
user_is_platform_admin,
)
from app.utils.user import user_has_permissions, user_is_platform_admin
PLATFORM_ADMIN_SERVICE_PERMISSIONS = OrderedDict(
[
@@ -120,81 +113,6 @@ def service_name_change(service_id):
)
@main.route(
"/services/<uuid:service_id>/service-settings/request-to-go-live/estimate-usage",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
def estimate_usage(service_id):
form = EstimateUsageForm(
volume_email=current_service.volume_email,
volume_sms=current_service.volume_sms,
consent_to_research={
True: "yes",
False: "no",
}.get(current_service.consent_to_research),
)
if form.validate_on_submit():
current_service.update(
volume_email=form.volume_email.data,
volume_sms=form.volume_sms.data,
consent_to_research=(form.consent_to_research.data == "yes"),
)
return redirect(
url_for(
"main.request_to_go_live",
service_id=service_id,
)
)
return render_template(
"views/service-settings/estimate-usage.html",
form=form,
)
@main.route(
"/services/<uuid:service_id>/service-settings/request-to-go-live", methods=["GET"]
)
@user_has_permissions("manage_service")
def request_to_go_live(service_id):
if current_service.live:
return render_template("views/service-settings/service-already-live.html")
return render_template("views/service-settings/request-to-go-live.html")
@main.route(
"/services/<uuid:service_id>/service-settings/request-to-go-live", methods=["POST"]
)
@user_has_permissions("manage_service")
@user_is_gov_user
def submit_request_to_go_live(service_id):
ticket_message = render_template("support-tickets/go-live-request.txt") + "\n"
ticket = NotifySupportTicket(
subject=f"Request to go live - {current_service.name}",
message=ticket_message,
ticket_type=NotifySupportTicket.TYPE_QUESTION,
user_name=current_user.name,
user_email=current_user.email_address,
requester_sees_message_content=False,
org_id=current_service.organization_id,
org_type=current_service.organization_type,
service_id=current_service.id,
)
zendesk_client.send_ticket_to_zendesk(ticket)
current_service.update(go_live_user=current_user.id)
flash(
"Thanks for your request to go live. Well get back to you within one working day.",
"default",
)
return redirect(url_for(".service_settings", service_id=service_id))
@main.route(
"/services/<uuid:service_id>/service-settings/switch-live", methods=["GET", "POST"]
)

View File

@@ -113,9 +113,6 @@ class ServiceEvent(Event):
def format_service_callback_api(self):
return "Updated the callback for delivery receipts"
def format_go_live_user(self):
return "Requested for this service to go live"
class APIKeyEvent(Event):
relevant = True

View File

@@ -1,3 +0,0 @@
QUESTION_TICKET_TYPE = "ask-question-give-feedback"
PROBLEM_TICKET_TYPE = "report-problem"
GENERAL_TICKET_TYPE = "general"

View File

@@ -25,7 +25,6 @@ class Organization(JSONModel, SortByNameMixin):
"active",
"organization_type",
"domains",
"request_to_go_live_notes",
"count_of_live_services",
"billing_contact_email_addresses",
"billing_contact_names",
@@ -71,7 +70,6 @@ class Organization(JSONModel, SortByNameMixin):
self.name = None
self.domains = []
self.organization_type = None
self.request_to_go_live_notes = None
@property
def organization_type_label(self):

View File

@@ -190,6 +190,15 @@ class Service(JSONModel, SortByNameMixin):
invited_user_id=str(invited_user_id),
)
def resend_invite(self, invited_user_id):
if str(invited_user_id) not in {user.id for user in self.invited_users}:
abort(404)
return invite_api_client.resend_invite(
service_id=self.id,
invited_user_id=str(invited_user_id),
)
def get_team_member(self, user_id):
if str(user_id) not in {user.id for user in self.active_users}:
abort(404)
@@ -370,22 +379,6 @@ class Service(JSONModel, SortByNameMixin):
)
)
@property
def go_live_checklist_completed(self):
return all(
(
bool(self.volumes),
self.has_team_members,
self.has_templates,
not self.needs_to_add_email_reply_to_address,
not self.needs_to_change_sms_sender,
)
)
@property
def go_live_checklist_completed_as_yes_no(self):
return "Yes" if self.go_live_checklist_completed else "No"
@cached_property
def free_sms_fragment_limit(self):
return billing_api_client.get_free_sms_fragment_limit_for_year(self.id) or 0

View File

@@ -38,12 +38,7 @@ class Navigation:
class HeaderNavigation(Navigation):
mapping = {
"support": {
"bat_phone",
"feedback",
"support",
"support_public",
"thanks",
"triage",
},
"features": {
"features",
@@ -101,9 +96,7 @@ class HeaderNavigation(Navigation):
"manage_users",
"remove_user_from_service",
"usage",
"estimate_usage",
"link_service_to_organization",
"request_to_go_live",
"service_add_email_reply_to",
"service_add_sms_sender",
"service_confirm_delete_email_reply_to",
@@ -127,7 +120,6 @@ class HeaderNavigation(Navigation):
"set_free_sms_allowance",
"set_message_limit",
"set_rate_limit",
"submit_request_to_go_live",
},
"pricing": {
"how_to_pay",
@@ -245,9 +237,7 @@ class MainNavigation(Navigation):
"usage",
},
"settings": {
"estimate_usage",
"link_service_to_organization",
"request_to_go_live",
"service_add_email_reply_to",
"service_add_sms_sender",
"service_confirm_delete_email_reply_to",
@@ -271,7 +261,6 @@ class MainNavigation(Navigation):
"set_free_sms_allowance",
"set_message_limit",
"set_rate_limit",
"submit_request_to_go_live",
},
"api-integration": {
"api_callbacks",
@@ -316,7 +305,6 @@ class OrgNavigation(Navigation):
"settings": {
"edit_organization_billing_details",
"edit_organization_domains",
"edit_organization_go_live_notes",
"edit_organization_name",
"edit_organization_notes",
"edit_organization_type",

View File

@@ -32,11 +32,11 @@ class InviteApiClient(NotifyAdminAPIClient):
"folder_permissions": folder_permissions,
}
data = _attach_current_user(data)
resp = self.post(url="/service/{}/invite".format(service_id), data=data)
resp = self.post(url=f"/service/{service_id}/invite", data=data)
return resp["data"]
def get_invites_for_service(self, service_id):
return self.get("/service/{}/invite".format(service_id))["data"]
return self.get(f"/service/{service_id}/invite")["data"]
def get_invited_user(self, invited_user_id):
return self.get(f"/invite/service/{invited_user_id}")["data"]
@@ -46,7 +46,7 @@ class InviteApiClient(NotifyAdminAPIClient):
def get_count_of_invites_with_permission(self, service_id, permission):
if permission not in all_ui_permissions:
raise TypeError("{} is not a valid permission".format(permission))
raise TypeError(f"{permission} is not a valid permission")
return len(
[
invited_user
@@ -56,22 +56,21 @@ class InviteApiClient(NotifyAdminAPIClient):
)
def check_token(self, token):
return self.get(url="/invite/service/check/{}".format(token))["data"]
return self.get(url=f"/invite/service/check/{token}")["data"]
def cancel_invited_user(self, service_id, invited_user_id):
data = {"status": "cancelled"}
data = _attach_current_user(data)
self.post(
url="/service/{0}/invite/{1}".format(service_id, invited_user_id), data=data
)
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)
def resend_invite(self, service_id, invited_user_id):
self.post(url=f"/service/{service_id}/invite/{invited_user_id}/resend", data={})
@cache.delete("service-{service_id}")
@cache.delete("user-{invited_user_id}")
def accept_invite(self, service_id, invited_user_id):
data = {"status": "accepted"}
self.post(
url="/service/{0}/invite/{1}".format(service_id, invited_user_id), data=data
)
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)
invite_api_client = InviteApiClient()

View File

@@ -103,14 +103,32 @@ class JobApiClient(NotifyAdminAPIClient):
return scheduled_for
def create_job(self, job_id, service_id, scheduled_for=None):
def create_job(
self,
job_id,
service_id,
scheduled_for=None,
template_id=None,
original_file_name=None,
notification_count=None,
valid=None,
):
data = {"id": job_id}
# make a datetime object in the user's preferred timezone
if scheduled_for:
scheduled_for = JobApiClient.convert_user_time_to_utc(scheduled_for)
data.update({"scheduled_for": scheduled_for})
data["scheduled_for"] = scheduled_for
if template_id:
data["template_id"] = template_id
if original_file_name:
data["original_file_name"] = original_file_name
if notification_count:
data["notification_count"] = notification_count
if valid:
data["valid"] = valid
data = _attach_current_user(data)
job = self.post(url="/service/{}/job".format(service_id), data=data)

View File

@@ -19,17 +19,22 @@
class="form-group{% if field.errors %} form-group-error{% endif %} {{ extra_form_group_classes }}"
data-module="{% if autofocus %}autofocus{% elif colour_preview %}colour-preview{% endif %}"
>
{% if field.errors %}
<div class="usa-alert usa-alert--error edit-textbox-error-mt" role="alert">
<div class="usa-alert__body">
<h4 class="usa-alert__heading">Error message</h4>
<p class="usa-alert__text" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
</p>
</div>
</div>
{% endif %}
<label class="usa-label" for="{{ field.name }}">
{% if label %}
{{ label }}
{% else %}
{{ field.label.text }}
{% endif %}
{% if field.errors %}
<span class="error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
</span>
{% endif %}
</label>
{% if hint %}
<div class="usa-hint">

View File

@@ -1,31 +0,0 @@
{% set service = current_service -%}
{% set organization = service.organization -%}
{% set user = current_user -%}
Service: {{ service.name }}
{{ url_for('main.service_dashboard', service_id=service.id, _external=True) }}
---
Organization type: {{ service.organization_type_label }}
{%- if organization.name %} (organization is {{ organization.name }})
{%- else %} (domain is {{ user.email_domain }})
{%- endif %}.
{%- if organization.request_to_go_live_notes %} {{ organization.request_to_go_live_notes }}{% endif %}
{%- if organization.agreement_signed_by %}
Agreement signed by: {{ organization.agreement_signed_by.email_address }}
{% endif -%}
{%- if organization.agreement_signed_on_behalf_of_email_address -%}
Agreement signed on behalf of: {{ organization.agreement_signed_on_behalf_of_email_address }}
{%- endif %}
Emails in next year: {{ service.volume_email|format_thousands }}
Text messages in next year: {{ service.volume_sms|format_thousands }}
Consent to research: {{ service.consent_to_research|format_yes_no }}
Other live services for that user: {{ user.live_services|format_yes_no }}
Service reply-to address: {{ service.default_email_reply_to_address or "not set" }}
---
Request sent by {{ user.email_address }}
Requesters user page: {{ url_for('main.user_information', user_id=user.id, _external=True) }}

View File

@@ -1,5 +0,0 @@
{{ content }}
{% if current_service -%}
Service: "{{ current_service.name }}"
{{ url_for('main.service_dashboard', service_id=current_service.id, _external=True) }}
{% endif %}

View File

@@ -10,7 +10,12 @@ Error
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": back_link }) }}
<!--hide back link for one-off sends because the user never created a csv file-->
{% if recipients|length == 1 and not recipients.allowed_to_send_to and not recipients.missing_column_headers %}
<!--do nothing-->
{% else %}
{{ usaBackLink({ "href": back_link }) }}
{% endif %}
{% endblock %}
{% block maincolumn_content %}
@@ -130,7 +135,10 @@ Error
{% endcall %}
</div>
<!--hide the upload button and back to top link for one off sends-->
{% if recipients|length == 1 and not recipients.allowed_to_send_to and not recipients.missing_column_headers %}
<!-- do nothing -->
{% else %}
<div class="js-stick-at-top-when-scrolling">
<div class="form-group">
{% if not request.args.from_test %}
@@ -144,6 +152,7 @@ Error
</div>
<a href="#content" class="usa-link back-to-top-link">Back to top</a>
</div>
{% endif %}
{% if not request.args.from_test %}
@@ -210,4 +219,4 @@ recipients.column_headers %}
<h2 class="font-body-lg">Preview of {{ template.name }}</h2>
{{ template|string }}
{% endblock %}
{% endblock %}

View File

@@ -53,7 +53,7 @@
{% if not current_user.is_authenticated or not current_service %}
<p>When youre ready to send messages to people outside your team, go to the <b class="bold">Settings</b> page and select <b class="bold">Request to go live</b>. Well approve your request within one working day.</p>
{% else %}
<p>You should <a class="usa-link" href="{{ url_for('.request_to_go_live', service_id=current_service.id) }}">request to go live</a> when youre ready to send messages to people outside your team. Well approve your request within one working day.</p>
<p>You should <a class="usa-link" href="{{ url_for('.support') }}">request to go live</a> when youre ready to send messages to people outside your team. Well approve your request within one working day.</p>
{% endif %}
<!-- <p>Check <a class="usa-link" href="{{ url_for('main.how_to_pay') }}">how to pay</a> if youre planning to exceed the <a class="usa-link" href="{{ url_for('.pricing', _anchor='text-messages') }}">free text message allowance</a>.</p> -->
</li>

View File

@@ -43,6 +43,8 @@
<span class="live-search-relevant">{{ user.email_address }}</span> (invited)
{%- elif user.status == 'cancelled' -%}
<span class="live-search-relevant">{{ user.email_address }}</span> (cancelled invite)
{%- elif user.status == 'expired' -%}
<span class="live-search-relevant">{{ user.email_address }}</span> (expired invite)
{%- elif user.id == current_user.id -%}
<span class="live-search-relevant">(you)</span>
{% else %}
@@ -84,6 +86,8 @@
{% if current_user.has_permissions('manage_service') %}
{% if user.status == 'pending' %}
<a class="user-list-edit-link usa-link" href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=user.id)}}">Cancel invitation<span class="usa-sr-only"> for {{ user.email_address }}</span></a>
{% elif user.status == 'expired' %}
<a class="user-list-edit-link usa-link" href="{{ url_for('.resend_invite', service_id=current_service.id, invited_user_id=user.id)}}">Resend invite<span class="usa-sr-only"> for {{ user.email_address }}</span></a>
{% elif user.is_editable_by(current_user) %}
<a class="user-list-edit-link usa-link" href="{{ url_for('.edit_user_permissions', service_id=current_service.id, user_id=user.id)}}">Change details<span class="usa-sr-only"> for {{ user.name }} {{ user.email_address }}</span></a>
{% endif %}

View File

@@ -1,30 +0,0 @@
{% extends "org_template.html" %}
{% from "components/form.html" import form_wrapper %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/page-header.html" import page_header %}
{% from "components/textbox.html" import textbox %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% block org_page_title %}
Edit request to go live notes
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": url_for('.organization_settings', org_id=current_org.id) }) }}
{% endblock %}
{% block maincolumn_content %}
{{ page_header("Edit request to go live notes") }}
<div class="grid-row">
<div class="grid-col-10">
<p>
Text entered here will be displayed in the Zendesk ticket when a service
belonging to this organization requests to go live.
</p>
{% call form_wrapper() %}
{{ textbox(form.request_to_go_live_notes, width='1-1', rows=3, autosize=True) }}
{{ page_footer('Save') }}
{% endcall %}
</div>
</div>
{% endblock %}

View File

@@ -34,16 +34,6 @@
)
}}
{% endcall %}
{% call row() %}
{{ text_field('Request to go live notes') }}
{{ optional_text_field(current_org.request_to_go_live_notes, default='None') }}
{{ edit_field(
'Change',
url_for('.edit_organization_go_live_notes', org_id=current_org.id),
suffix='go live notes for the organization'
)
}}
{% endcall %}
{% call row() %}
{{ text_field('Billing details')}}

View File

@@ -206,7 +206,7 @@
</p>
<p>
Problems or comments?
<a class="usa-link" href="{{ url_for('main.support') }}">Give feedback</a>.
<a class="usa-link" href="{{ url_for('main.support') }}">Contact us</a>.
</p>
{% endif %}

View File

@@ -1,42 +0,0 @@
{% extends "withnav_template.html" %}
{% from "components/banner.html" import banner_wrapper %}
{% from "components/form.html" import form_wrapper %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% block service_page_title %}
Tell us how many messages you expect to send
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": url_for('main.request_to_go_live', service_id=current_service.id) }) }}
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="grid-col-12">
{% if not form.at_least_one_volume_filled %}
{% call banner_wrapper(type='dangerous') %}
<h1 class='banner-title'>
Enter the number of messages you expect to send in the next year
</h1>
{% endcall %}
{% else %}
{{ page_header('Tell us how many messages you expect to send') }}
{% endif %}
{% call form_wrapper() %}
<div class="form-group">
{{ form.volume_email(param_extensions={
"hint": {"text": "For example, 50,000"},
}) }}
{{ form.volume_sms(param_extensions={
"hint": {"text": "For example, 50,000"},
}) }}
</div>
{{ form.consent_to_research }}
{{ page_footer('Continue') }}
{% endcall %}
</div>
</div>
{% endblock %}

View File

@@ -1,75 +0,0 @@
{% extends "withnav_template.html" %}
{% from "components/form.html" import form_wrapper %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/task-list.html" import task_list_wrapper, task_list_item %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% block service_page_title %}
Before you request to go live
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": url_for('main.service_settings', service_id=current_service.id) }) }}
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="grid-col-12">
{{ page_header('Before you request to go live') }}
{% call task_list_wrapper() %}
{{ task_list_item(
current_service.has_estimated_usage,
'Tell us how many messages you expect to send',
url_for('main.estimate_usage', service_id=current_service.id),
) }}
{{ task_list_item(
current_service.has_team_members,
'Add a team member who can manage settings, team and usage',
url_for('main.manage_users', service_id=current_service.id),
) }}
{{ task_list_item(
current_service.has_templates,
'Add templates with examples of the content you plan to send',
url_for('main.choose_template', service_id=current_service.id),
) }}
{% if current_service.intending_to_send_email %}
{{ task_list_item(
current_service.has_email_reply_to_address,
'Add a reply-to email address',
url_for('main.service_email_reply_to', service_id=current_service.id),
) }}
{% endif %}
{% if (
current_service.intending_to_send_sms
and current_service.shouldnt_use_govuk_as_sms_sender
) %}
{{ task_list_item(
not current_service.sms_sender_is_govuk,
'Change your text message sender name',
url_for('main.service_sms_senders', service_id=current_service.id),
) }}
{% endif %}
{% endcall %}
{% if not current_user.is_gov_user %}
<p>
Only team members with a government email address can request to go live.
</p>
{% elif (not current_service.go_live_checklist_completed) %}
<p>
You must complete these steps before you can request to go live.
</p>
{% else %}
<p>
When we receive your request well get back to you within one working day.
</p>
<p class="bottom-gutter">
By requesting to go live youre agreeing to our <a class="usa-link" href="{{ url_for('.terms') }}">terms of use</a>.
</p>
{% call form_wrapper() %}
{{ page_footer('Request to go live') }}
{% endcall %}
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -1,28 +0,0 @@
{% extends "withnav_template.html" %}
{% from "components/page-header.html" import page_header %}
{% block service_page_title %}
Your service is already live
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="grid-col-12">
{{ page_header('Your service is already live') }}
<p>
{% if current_service.go_live_at %}
{{ current_service.name }} went live on {{ current_service.go_live_at | format_date_normal }}.
{% else %}
{{ current_service.name }} is already live.
{% endif %}
</p>
<p>
<a class="usa-link" href="{{ url_for('.choose_account') }}">Switch service</a>
if you want to make a different service live.
</p>
</div>
</div>
{% endblock %}

View File

@@ -1,46 +0,0 @@
{% extends "withoutnav_template.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/page-header.html" import page_header %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% block per_page_title %}
Out of hours emergencies
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": url_for('.support') }) }}
{% endblock %}
{% block maincolumn_content %}
{{ page_header('Out of hours emergencies')}}
<div class="grid-row">
<div class="grid-col-8">
<p>
First, check the
<a class="usa-link" href="https://status.notifications.service.gov.uk">system status page</a>.
You do not need to contact us if
the problem youre having is listed on that page.
</p>
<p>
Otherwise, contact us using the emergency email address we
gave you or your service manager when we made your service live.
</p>
<p>
Well reply within 30 minutes and give you hourly updates
until the problems fixed.
</p>
<p>
We do not offer out of hours support if your service is in
trial mode.
</p>
<h2 class="font-body-lg">Any other problems</h2>
<p class="bottom-gutter-2">
<a class="usa-link" href="{{ url_for('main.feedback', ticket_type='report-problem', severe='no') }}">Fill in this form</a>
and well get back to you by the next working day.
</p>
</div>
</div>
{% endblock %}

View File

@@ -1,42 +0,0 @@
{% extends "withoutnav_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import sticky_page_footer %}
{% from "components/page-header.html" import page_header %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% block per_page_title %}
{{ page_title }}
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": back_link }) }}
{% endblock %}
{% block maincolumn_content %}
{{ page_header(page_title) }}
<div class="grid-row">
<div class="grid-col-8">
{% if show_status_page_banner %}
<div class="panel panel-border-wide">
<p>
Check our <a class="usa-link" href="https://status.notifications.service.gov.uk">system status</a>
page to see if there are any known issues with Notify.gov.
</p>
</div>
{% endif %}
{% call form_wrapper() %}
{{ textbox(form.feedback, width='1-1', hint='', rows=10, autosize=True) }}
{% if not current_user.is_authenticated %}
{{ form.name(param_extensions={"classes": ""}) }}
{{ form.email_address(param_extensions={"classes": ""}) }}
{% else %}
<p>Well reply to {{ current_user.email_address }}</p>
{% endif %}
{{ sticky_page_footer('Send') }}
{% endcall %}
</div>
</div>
{% endblock %}

View File

@@ -1,50 +0,0 @@
{% extends "withoutnav_template.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% block per_page_title %}
The Notify.gov service is for people who work in the government
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": url_for('.support') }) }}
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="grid-col-8">
{{ page_header('The Notify.gov service is for people who work in the government') }}
<p>
We cannot give advice to the public. We do not have access to information about you held by government departments.
</p>
<p>
There are other pages on Notify.gov where you can get help:
</p>
<h2 class="govuk-heading-m">
<a class="usa-link" href="https://www.gov.uk/coronavirus">Coronavirus (COVID-19)</a>
</h2>
<p>
Find guidance and support.
</p>
<h2 class="govuk-heading-m">
<a class="usa-link" href="https://www.gov.uk/contact">Contact the government</a>
</h2>
<p>
Ask about benefits, driving, transport, tax, and more.
</p>
<h2 class="govuk-heading-m">
<a class="usa-link" href="https://www.gov.uk/report-suspicious-emails-websites-phishing">Report internet scams and phishing</a>
</h2>
<p>
Advice on suspicious emails and text messages.
</p>
</div>
</div>
{% endblock %}

View File

@@ -1,38 +0,0 @@
{% extends "withoutnav_template.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/page-header.html" import page_header %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% block per_page_title %}
Thanks for contacting us
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": url_for('.support') }) }}
{% endblock %}
{% block maincolumn_content %}
{{ page_header('Thanks for contacting us') }}
<p>
{% if out_of_hours_emergency %}
Well reply in the next 30 minutes.
{% else %}
{% if email_address_provided %}
{% if out_of_hours %}
Well reply within one working day.
{% else %}
Well aim to read your message in the next 30 minutes and well reply within one
working day.
{% endif %}
{% else %}
{% if out_of_hours %}
Well read your message when were back in the office.
{% else %}
Well aim to read your message in the next 30 minutes.
{% endif %}
{% endif %}
{% endif %}
</p>
{% endblock %}

View File

@@ -1,62 +0,0 @@
{% extends "withoutnav_template.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/page-header.html" import page_header %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
{% block per_page_title %}
{{ page_title }}
{% endblock %}
{% block backLink %}
{{ usaBackLink({ "href": url_for('.support') }) }}
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="grid-col-8">
{{ page_header(page_title) }}
{% call form_wrapper() %}
{{ form.severe }}
{{ page_footer('Continue') }}
{% endcall %}
<h2 class="heading-small">
Its only an emergency if:
</h2>
<ul class="list list-bullet">
<li>
no one in your team can log in
</li>
<li>
you get a technical difficulties error message when you try
to upload a file
</li>
<li>
you get a 500 response code when you try to send messages
using the API
</li>
</ul>
<h2 class="heading-small">
Its not an emergency if:
</h2>
<ul class="list list-bullet bottom-gutter">
<li>
all your messages stay in sending for a few hours
</li>
<li>
you send the wrong message by accident
</li>
<li>
a team member uses Notify.gov to send an
inappropriate message
</li>
<li>
your system is telling the Notify.gov API to send the wrong
message
</li>
</ul>
</div>
</div>
{% endblock %}

View File

@@ -17,7 +17,7 @@
{% if current_service and current_service.trial_mode %}
<p>
To remove these restrictions, you can <a class="usa-link" href="{{ url_for('.request_to_go_live', service_id=current_service.id) }}">request to go live</a>.</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:
@@ -38,6 +38,6 @@
<li>update your settings so youre ready to send and receive messages</li>
<li>accept our terms of use</li>
</ul>
{% endblock %}

View File

@@ -1,10 +1,5 @@
from werkzeug.routing import BaseConverter
from app.models.feedback import (
GENERAL_TICKET_TYPE,
PROBLEM_TICKET_TYPE,
QUESTION_TICKET_TYPE,
)
from app.models.service import Service
@@ -12,9 +7,5 @@ class TemplateTypeConverter(BaseConverter):
regex = "(?:{})".format("|".join(Service.TEMPLATE_TYPES))
class TicketTypeConverter(BaseConverter):
regex = f"(?:{PROBLEM_TICKET_TYPE}|{QUESTION_TICKET_TYPE}|{GENERAL_TICKET_TYPE})"
class SimpleDateTypeConverter(BaseConverter):
regex = r"([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))"