Merge branch 'main' into 976-convert-start-pages-to-generate-markdown

This commit is contained in:
Anastasia Gradova
2023-12-22 11:10:17 -07:00
125 changed files with 783 additions and 8249 deletions

View File

@@ -9,7 +9,6 @@ from app.main.views import ( # noqa isort:skip
code_not_received,
conversation,
dashboard,
email_branding,
feedback,
find_services,
find_users,

View File

@@ -61,9 +61,8 @@ 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 branding, merge_jsonlike
from app.utils import merge_jsonlike
from app.utils.csv import get_user_preferred_timezone
from app.utils.user_permissions import all_ui_permissions, permission_options
@@ -815,49 +814,6 @@ class GovukCheckboxField(BooleanField):
)
class GovukTextareaField(TextAreaField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(TextAreaField, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
# error messages
error_message = None
if field.errors:
error_message = {"text": field.errors[0]}
params = {
"name": field.name,
"id": field.id,
"rows": 8,
"label": {
"text": field.label.text,
"classes": None,
"isPageHeading": False,
},
"hint": {"text": None},
"errorMessage": error_message,
}
# extend default params with any sent in during instantiation
if self.param_extensions:
merge_jsonlike(params, self.param_extensions)
# add any sent in though use in templates
if param_extensions:
merge_jsonlike(params, param_extensions)
return Markup(
render_template(
"components/components/textarea/template.njk", params=params
)
)
# based on work done by @richardjpope: https://github.com/richardjpope/recourse/blob/master/recourse/forms.py#L6
class GovukCheckboxesField(SelectMultipleField):
render_as_list = False
@@ -1362,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?",
@@ -1543,65 +1456,6 @@ class ServiceSwitchChannelForm(ServiceOnOffSettingForm):
super().__init__(name, *args, **kwargs)
class AdminSetEmailBrandingForm(StripWhitespaceForm):
branding_style = GovukRadiosFieldWithNoneOption(
"Branding style",
param_extensions={"fieldset": {"legend": {"classes": "usa-sr-only"}}},
thing="a branding style",
)
DEFAULT = (FieldWithNoneOption.NONE_OPTION_VALUE, "GOV.UK")
def __init__(self, all_branding_options, current_branding):
super().__init__(branding_style=current_branding)
self.branding_style.choices = sorted(
all_branding_options + [self.DEFAULT],
key=lambda branding: (
branding[0] != current_branding,
branding[0] is not self.DEFAULT[0],
branding[1].lower(),
),
)
class AdminPreviewBrandingForm(StripWhitespaceForm):
branding_style = HiddenFieldWithNoneOption("branding_style")
class AdminEditEmailBrandingForm(StripWhitespaceForm):
name = GovukTextInputField("Name of brand")
text = GovukTextInputField("Text")
colour = GovukTextInputField(
"Colour",
validators=[
Regexp(
regex="^$|^#(?:[0-9a-fA-F]{3}){1,2}$",
message="Must be a valid color hex code (starting with #)",
)
],
param_extensions={
"attributes": {"data-module": "colour-preview"},
},
)
file = FileField_wtf(
"Upload a PNG logo", validators=[FileAllowed(["png"], "PNG Images only!")]
)
brand_type = GovukRadiosField(
"Brand type",
choices=[
("both", "GOV.UK and branding"),
("org", "Branding only"),
("org_banner", "Branding banner"),
],
)
def validate_name(self, name):
op = request.form.get("operation")
if op == "email-branding-details" and not self.name.data:
raise ValidationError("This field is required")
class SVGFileUpload(StripWhitespaceForm):
file = FileField_wtf(
"Upload an SVG logo",
@@ -1816,42 +1670,6 @@ class AdminSetOrganizationForm(StripWhitespaceForm):
)
class ChooseBrandingForm(StripWhitespaceForm):
FALLBACK_OPTION_VALUE = "something_else"
FALLBACK_OPTION = (FALLBACK_OPTION_VALUE, "Something else")
@property
def something_else_is_only_option(self):
return self.options.choices == (self.FALLBACK_OPTION,)
class ChooseEmailBrandingForm(ChooseBrandingForm):
options = RadioField("Choose your new email branding")
def __init__(self, service):
super().__init__()
self.options.choices = tuple(
list(branding.get_email_choices(service)) + [self.FALLBACK_OPTION]
)
class SomethingElseBrandingForm(StripWhitespaceForm):
something_else = GovukTextareaField(
"Describe the branding you want",
validators=[DataRequired("Cannot be empty")],
param_extensions={
"label": {
"isPageHeading": True,
"classes": "font-body-xl",
},
"hint": {
"text": "Include links to your brand guidelines or examples of how to use your branding."
},
},
)
class AdminServiceAddDataRetentionForm(StripWhitespaceForm):
notification_type = GovukRadiosField(
"What notification type?",
@@ -2043,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,133 +0,0 @@
from flask import current_app, redirect, render_template, session, url_for
from app import email_branding_client
from app.main import main
from app.main.forms import AdminEditEmailBrandingForm, SearchByNameForm
from app.s3_client.s3_logo_client import (
TEMP_TAG,
delete_email_temp_file,
delete_email_temp_files_created_by,
permanent_email_logo_name,
persist_logo,
upload_email_logo,
)
from app.utils.user import user_is_platform_admin
@main.route("/email-branding", methods=["GET", "POST"])
@user_is_platform_admin
def email_branding():
brandings = email_branding_client.get_all_email_branding(sort_key="name")
return render_template(
"views/email-branding/select-branding.html",
email_brandings=brandings,
search_form=SearchByNameForm(),
)
@main.route("/email-branding/<uuid:branding_id>/edit", methods=["GET", "POST"])
@main.route("/email-branding/<uuid:branding_id>/edit/<logo>", methods=["GET", "POST"])
@user_is_platform_admin
def update_email_branding(branding_id, logo=None):
email_branding = email_branding_client.get_email_branding(branding_id)[
"email_branding"
]
form = AdminEditEmailBrandingForm(
name=email_branding["name"],
text=email_branding["text"],
colour=email_branding["colour"],
brand_type=email_branding["brand_type"],
)
logo = logo if logo else email_branding.get("logo") if email_branding else None
if form.validate_on_submit():
if form.file.data:
upload_filename = upload_email_logo(
form.file.data.filename, form.file.data, user_id=session["user_id"]
)
if logo and logo.startswith(TEMP_TAG.format(user_id=session["user_id"])):
delete_email_temp_file(logo)
return redirect(
url_for(
".update_email_branding",
branding_id=branding_id,
logo=upload_filename,
)
)
updated_logo_name = (
permanent_email_logo_name(logo, session["user_id"]) if logo else None
)
email_branding_client.update_email_branding(
branding_id=branding_id,
logo=updated_logo_name,
name=form.name.data,
text=form.text.data,
colour=form.colour.data,
brand_type=form.brand_type.data,
)
if logo:
persist_logo(logo, updated_logo_name)
delete_email_temp_files_created_by(session["user_id"])
return redirect(url_for(".email_branding", branding_id=branding_id))
return render_template(
"views/email-branding/manage-branding.html",
form=form,
email_branding=email_branding,
cdn_url=current_app.config["LOGO_CDN_DOMAIN"],
logo=logo,
)
@main.route("/email-branding/create", methods=["GET", "POST"])
@main.route("/email-branding/create/<logo>", methods=["GET", "POST"])
@user_is_platform_admin
def create_email_branding(logo=None):
form = AdminEditEmailBrandingForm(brand_type="org")
if form.validate_on_submit():
if form.file.data:
upload_filename = upload_email_logo(
form.file.data.filename, form.file.data, user_id=session["user_id"]
)
if logo and logo.startswith(TEMP_TAG.format(user_id=session["user_id"])):
delete_email_temp_file(logo)
return redirect(url_for(".create_email_branding", logo=upload_filename))
updated_logo_name = (
permanent_email_logo_name(logo, session["user_id"]) if logo else None
)
email_branding_client.create_email_branding(
logo=updated_logo_name,
name=form.name.data,
text=form.text.data,
colour=form.colour.data,
brand_type=form.brand_type.data,
)
if logo:
persist_logo(logo, updated_logo_name)
delete_email_temp_files_created_by(session["user_id"])
return redirect(url_for(".email_branding"))
return render_template(
"views/email-branding/manage-branding.html",
form=form,
cdn_url=current_app.config["LOGO_CDN_DOMAIN"],
logo=logo,
)

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

@@ -1,19 +1,9 @@
from flask import (
abort,
current_app,
make_response,
redirect,
render_template,
request,
url_for,
)
from flask import abort, redirect, render_template, request, url_for
from flask_login import current_user
from notifications_utils.template import HTMLEmailTemplate
from app import email_branding_client, status_api_client
from app import status_api_client
from app.formatters import convert_markdown_template
from app.main import main
from app.main.forms import FieldWithNoneOption
from app.main.views.pricing import CURRENT_SMS_RATE
from app.main.views.sub_navigation_dictionaries import features_nav, using_notify_nav
from app.utils.user import user_is_logged_in
@@ -64,105 +54,6 @@ def design_content():
)
@main.route("/_email")
@user_is_logged_in
def email_template():
branding_type = "govuk"
branding_style = request.args.get("branding_style", None)
if branding_style == FieldWithNoneOption.NONE_OPTION_VALUE:
branding_style = None
if branding_style is not None:
email_branding = email_branding_client.get_email_branding(branding_style)[
"email_branding"
]
branding_type = email_branding["brand_type"]
if branding_type == "govuk":
brand_text = None
brand_colour = None
brand_logo = None
govuk_banner = True
brand_banner = False
brand_name = None
else:
colour = email_branding["colour"]
brand_text = email_branding["text"]
brand_colour = colour
brand_logo = (
f"https://{current_app.config['LOGO_CDN_DOMAIN']}/{email_branding['logo']}"
if email_branding["logo"]
else None
)
govuk_banner = branding_type in ["govuk", "both"]
brand_banner = branding_type == "org_banner"
brand_name = email_branding["name"]
template = {
"template_type": "email",
"subject": "Email branding preview",
"content": (
"Lorem Ipsum is simply dummy text of the printing and typesetting "
"industry.\n\nLorem Ipsum has been the industrys standard dummy "
"text ever since the 1500s, when an unknown printer took a galley "
"of type and scrambled it to make a type specimen book. "
"\n\n"
"# History"
"\n\n"
"It has "
"survived not only"
"\n\n"
"* five centuries"
"\n"
"* but also the leap into electronic typesetting"
"\n\n"
"It was "
"popularised in the 1960s with the release of Letraset sheets "
"containing Lorem Ipsum passages, and more recently with desktop "
"publishing software like Aldus PageMaker including versions of "
"Lorem Ipsum."
"\n\n"
"^ It is a long established fact that a reader will be distracted "
"by the readable content of a page when looking at its layout."
"\n\n"
"The point of using Lorem Ipsum is that it has a more-or-less "
"normal distribution of letters, as opposed to using Content "
"here, content here, making it look like readable English."
"\n\n\n"
"1. One"
"\n"
"2. Two"
"\n"
"10. Three"
"\n\n"
"This is an example of an email sent using Notify.gov."
"\n\n"
"https://www.notifications.service.gov.uk"
),
}
if not bool(request.args):
resp = make_response(str(HTMLEmailTemplate(template)))
else:
resp = make_response(
str(
HTMLEmailTemplate(
template,
govuk_banner=govuk_banner,
brand_text=brand_text,
brand_colour=brand_colour,
brand_logo=brand_logo,
brand_banner=brand_banner,
brand_name=brand_name,
)
)
)
resp.headers["X-Frame-Options"] = "SAMEORIGIN"
return resp
@main.route("/documentation")
@user_is_logged_in
def documentation():
@@ -197,14 +88,6 @@ def roadmap():
return render_template("views/roadmap.html", navigation_links=features_nav())
@main.route("/features/email")
@user_is_logged_in
def features_email():
return render_template(
"views/features/emails.html", navigation_links=features_nav()
)
@main.route("/features/sms")
@user_is_logged_in
def features_sms():
@@ -290,15 +173,6 @@ def guidance_index():
)
@main.route("/using-notify/guidance/branding-and-customisation")
@user_is_logged_in
def branding_and_customisation():
return render_template(
"views/guidance/branding-and-customisation.html",
navigation_links=using_notify_nav(),
)
@main.route("/using-notify/guidance/create-and-send-messages")
@user_is_logged_in
def create_and_send_messages():

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

@@ -6,21 +6,13 @@ from flask import flash, redirect, render_template, request, url_for
from flask_login import current_user
from notifications_python_client.errors import HTTPError
from app import (
current_organization,
email_branding_client,
org_invite_api_client,
organizations_client,
)
from app import current_organization, org_invite_api_client, organizations_client
from app.main import main
from app.main.forms import (
AdminBillingDetailsForm,
AdminNewOrganizationForm,
AdminNotesForm,
AdminOrganizationDomainsForm,
AdminOrganizationGoLiveNotesForm,
AdminPreviewBrandingForm,
AdminSetEmailBrandingForm,
InviteOrgUserForm,
OrganizationOrganizationTypeForm,
RenameOrganizationForm,
@@ -31,7 +23,6 @@ from app.main.views.dashboard import (
get_tuples_of_financial_years,
requested_and_current_financial_year,
)
from app.main.views.service_settings import get_branding_as_value_and_label
from app.models.organization import AllOrganizations, Organization
from app.models.user import InvitedOrgUser, User
from app.utils.csv import Spreadsheet
@@ -283,58 +274,6 @@ def edit_organization_type(org_id):
)
@main.route(
"/organizations/<uuid:org_id>/settings/set-email-branding", methods=["GET", "POST"]
)
@user_is_platform_admin
def edit_organization_email_branding(org_id):
email_branding = email_branding_client.get_all_email_branding()
form = AdminSetEmailBrandingForm(
all_branding_options=get_branding_as_value_and_label(email_branding),
current_branding=current_organization.email_branding_id,
)
if form.validate_on_submit():
return redirect(
url_for(
".organization_preview_email_branding",
org_id=org_id,
branding_style=form.branding_style.data,
)
)
return render_template(
"views/organizations/organization/settings/set-email-branding.html",
form=form,
search_form=SearchByNameForm(),
)
@main.route(
"/organizations/<uuid:org_id>/settings/preview-email-branding",
methods=["GET", "POST"],
)
@user_is_platform_admin
def organization_preview_email_branding(org_id):
branding_style = request.args.get("branding_style", None)
form = AdminPreviewBrandingForm(branding_style=branding_style)
if form.validate_on_submit():
current_organization.update(
email_branding_id=form.branding_style.data,
delete_services_cache=True,
)
return redirect(url_for(".organization_settings", org_id=org_id))
return render_template(
"views/organizations/organization/settings/preview-email-branding.html",
form=form,
action=url_for("main.organization_preview_email_branding", org_id=org_id),
)
@main.route(
"/organizations/<uuid:org_id>/settings/edit-organization-domains",
methods=["GET", "POST"],
@@ -373,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

@@ -596,13 +596,6 @@ def clear_cache():
"service-????????-????-????-????-????????????-template-????????-????-????-????-????????????-versions", # noqa
],
),
(
"email_branding",
[
"email_branding",
"email_branding-????????-????-????-????-????????????",
],
),
(
"organization",
[

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,12 +13,10 @@ 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,
current_service,
email_branding_client,
inbound_number_client,
notification_api_client,
organizations_client,
@@ -29,23 +27,18 @@ 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 (
AdminBillingDetailsForm,
AdminNotesForm,
AdminPreviewBrandingForm,
AdminServiceAddDataRetentionForm,
AdminServiceEditDataRetentionForm,
AdminServiceInboundNumberForm,
AdminServiceMessageLimitForm,
AdminServiceRateLimitForm,
AdminServiceSMSAllowanceForm,
AdminSetEmailBrandingForm,
AdminSetOrganizationForm,
ChooseEmailBrandingForm,
EstimateUsageForm,
RenameServiceForm,
SearchByNameForm,
ServiceContactDetailsForm,
@@ -55,16 +48,10 @@ from app.main.forms import (
ServiceSmsSenderForm,
ServiceSwitchChannelForm,
SMSPrefixForm,
SomethingElseBrandingForm,
)
from app.utils import DELIVERED_STATUSES, FAILURE_STATUSES, SENDING_STATUSES
from app.utils.branding import get_email_choices as get_email_branding_choices
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(
[
@@ -87,7 +74,6 @@ def service_settings(service_id):
return render_template(
"views/service-settings.html",
service_permissions=PLATFORM_ADMIN_SERVICE_PERMISSIONS,
email_branding_options=ChooseEmailBrandingForm(current_service),
)
@@ -127,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"]
)
@@ -876,57 +787,6 @@ def set_rate_limit(service_id):
)
@main.route(
"/services/<uuid:service_id>/service-settings/set-email-branding",
methods=["GET", "POST"],
)
@user_is_platform_admin
def service_set_email_branding(service_id):
email_branding = email_branding_client.get_all_email_branding()
form = AdminSetEmailBrandingForm(
all_branding_options=get_branding_as_value_and_label(email_branding),
current_branding=current_service.email_branding_id,
)
if form.validate_on_submit():
return redirect(
url_for(
".service_preview_email_branding",
service_id=service_id,
branding_style=form.branding_style.data,
)
)
return render_template(
"views/service-settings/set-email-branding.html",
form=form,
search_form=SearchByNameForm(),
)
@main.route(
"/services/<uuid:service_id>/service-settings/preview-email-branding",
methods=["GET", "POST"],
)
@user_is_platform_admin
def service_preview_email_branding(service_id):
branding_style = request.args.get("branding_style", None)
form = AdminPreviewBrandingForm(branding_style=branding_style)
if form.validate_on_submit():
current_service.update(email_branding=form.branding_style.data)
return redirect(url_for(".service_settings", service_id=service_id))
return render_template(
"views/service-settings/preview-email-branding.html",
form=form,
service_id=service_id,
action=url_for("main.service_preview_email_branding", service_id=service_id),
)
@main.route(
"/services/<uuid:service_id>/service-settings/link-service-to-organization",
methods=["GET", "POST"],
@@ -957,145 +817,6 @@ def link_service_to_organization(service_id):
)
def create_email_branding_zendesk_ticket(form_option_selected, detail=None):
form = ChooseEmailBrandingForm(current_service)
ticket_message = render_template(
"support-tickets/branding-request.txt",
current_branding=current_service.email_branding_name,
branding_requested=dict(form.options.choices)[form_option_selected],
detail=detail,
)
ticket = NotifySupportTicket(
subject=f"Email branding request - {current_service.name}",
message=ticket_message,
ticket_type=NotifySupportTicket.TYPE_QUESTION,
user_name=current_user.name,
user_email=current_user.email_address,
org_id=current_service.organization_id,
org_type=current_service.organization_type,
service_id=current_service.id,
)
zendesk_client.send_ticket_to_zendesk(ticket)
@main.route(
"/services/<uuid:service_id>/service-settings/email-branding",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
def email_branding_request(service_id):
form = ChooseEmailBrandingForm(current_service)
branding_name = current_service.email_branding_name
if form.validate_on_submit():
return redirect(
url_for(
f".email_branding_{form.options.data}",
service_id=current_service.id,
)
)
return render_template(
"views/service-settings/branding/email-branding-options.html",
form=form,
branding_name=branding_name,
)
def check_email_branding_allowed_for_service(branding):
allowed_branding_for_service = dict(get_email_branding_choices(current_service))
if branding not in allowed_branding_for_service:
abort(404)
@main.route(
"/services/<uuid:service_id>/service-settings/email-branding/govuk",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
def email_branding_govuk(service_id):
check_email_branding_allowed_for_service("govuk")
if request.method == "POST":
current_service.update(email_branding=None)
flash("Youve updated your email branding", "default")
return redirect(url_for(".service_settings", service_id=current_service.id))
return render_template("views/service-settings/branding/email-branding-govuk.html")
@main.route(
"/services/<uuid:service_id>/service-settings/email-branding/govuk-and-org",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
def email_branding_govuk_and_org(service_id):
check_email_branding_allowed_for_service("govuk_and_org")
if request.method == "POST":
create_email_branding_zendesk_ticket("govuk_and_org")
flash(
"Thanks for your branding request. Well get back to you within one working day.",
"default",
)
return redirect(url_for(".service_settings", service_id=current_service.id))
return render_template(
"views/service-settings/branding/email-branding-govuk-org.html"
)
@main.route(
"/services/<uuid:service_id>/service-settings/email-branding/organization",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
def email_branding_organization(service_id):
check_email_branding_allowed_for_service("organization")
if request.method == "POST":
create_email_branding_zendesk_ticket("organization")
flash(
"Thanks for your branding request. Well get back to you within one working day.",
"default",
)
return redirect(url_for(".service_settings", service_id=current_service.id))
return render_template(
"views/service-settings/branding/email-branding-organization.html"
)
@main.route(
"/services/<uuid:service_id>/service-settings/email-branding/something-else",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
def email_branding_something_else(service_id):
form = SomethingElseBrandingForm()
if form.validate_on_submit():
create_email_branding_zendesk_ticket(
"something_else", detail=form.something_else.data
)
flash(
"Thanks for your branding request. Well get back to you within one working day.",
"default",
)
return redirect(url_for(".service_settings", service_id=current_service.id))
return render_template(
"views/service-settings/branding/email-branding-something-else.html",
form=form,
branding_options=ChooseEmailBrandingForm(current_service),
)
@main.route("/services/<uuid:service_id>/data-retention", methods=["GET"])
@user_is_platform_admin
def data_retention(service_id):
@@ -1184,10 +905,6 @@ def edit_service_billing_details(service_id):
)
def get_branding_as_value_and_label(email_branding):
return [(branding["id"], branding["name"]) for branding in email_branding]
def convert_dictionary_to_wtforms_choices_format(dictionary, value, label):
return [(item[value], item[label]) for item in dictionary]

View File

@@ -4,10 +4,6 @@ def features_nav():
"name": "Features",
"link": "main.features",
"sub_navigation_items": [
# {
# "name": "Emails",
# "link": "main.features_email",
# },
# {
# "name": "Text messages",
# "link": "main.features_sms",
@@ -56,10 +52,6 @@ def using_notify_nav():
# "link": "main.edit_and_format_messages",
# },
# {
# "name": "Branding",
# "link": "main.branding_and_customisation",
# },
# {
# "name": "Send files by email",
# "link": "main.send_files_by_email",
# },