diff --git a/app/assets/images/tour-next.png b/app/assets/images/tour-next.png new file mode 100644 index 000000000..1cdfc8218 Binary files /dev/null and b/app/assets/images/tour-next.png differ diff --git a/app/assets/images/tour/2.png b/app/assets/images/tour/2.png new file mode 100644 index 000000000..4c4a82dbe Binary files /dev/null and b/app/assets/images/tour/2.png differ diff --git a/app/assets/images/tour/3.png b/app/assets/images/tour/3.png new file mode 100644 index 000000000..74d8342cb Binary files /dev/null and b/app/assets/images/tour/3.png differ diff --git a/app/assets/images/tour/4.png b/app/assets/images/tour/4.png new file mode 100644 index 000000000..07d917187 Binary files /dev/null and b/app/assets/images/tour/4.png differ diff --git a/app/assets/javascripts/highlightTags.js b/app/assets/javascripts/highlightTags.js index ca4359111..f4022710c 100644 --- a/app/assets/javascripts/highlightTags.js +++ b/app/assets/javascripts/highlightTags.js @@ -7,6 +7,25 @@ const tagPattern = /\(\([^\)\(]+\)\)/g; + const getPlaceholderHint = function(placeholders) { + if (0 === placeholders.length) { + return ` +
Add fields using ((double brackets))
+ Show me how + `; + } + if (1 === placeholders.length) { + return ` +Add fields using ((double brackets))
+You’ll populate the ‘${placeholders[0]}’ field when you send messages using this template
+ `; + } + return ` +Add fields using ((double brackets))
+You’ll populate your fields when you send some messages
+ `; + }; + Modules.HighlightTags = function() { this.start = function(textarea) { @@ -22,6 +41,9 @@ `)) .on("input", this.update); + this.$placeHolderHint = $('#placeholder-hint') + .on("click", ".placeholder-hint-action", this.demo); + this.initialHeight = this.$textbox.height(); this.$backgroundMaskForeground.width( @@ -40,14 +62,30 @@ ) ); + this.escapedMessage = () => $('').text(this.$textbox.val()).html(); + + this.listPlaceholders = () => this.escapedMessage().match(tagPattern) || []; + + this.listPlaceholdersWithoutBrackets = () => this.listPlaceholders().map( + placeholder => placeholder.substring(2, placeholder.length - 2) + ); + this.replacePlaceholders = () => this.$backgroundMaskForeground.html( - $('').text(this.$textbox.val()).html().replace( + this.escapedMessage().replace( tagPattern, match => `${match}` ) ); + this.hint = () => this.$placeHolderHint.html( + getPlaceholderHint(this.listPlaceholdersWithoutBrackets()) + ); + this.update = () => ( - this.replacePlaceholders() && this.resize() + this.replacePlaceholders() && this.resize() && this.hint() + ); + + this.demo = () => ( + this.$textbox.val((i, current) => `Dear ((name)), ${current}`) && this.update() ); }; diff --git a/app/assets/stylesheets/app.scss b/app/assets/stylesheets/app.scss index c5e88e8c9..6fbc015c6 100644 --- a/app/assets/stylesheets/app.scss +++ b/app/assets/stylesheets/app.scss @@ -127,4 +127,4 @@ a[rel="external"] { @include media(tablet) { @include external-link-19; } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/components/banner.scss b/app/assets/stylesheets/components/banner.scss index 540ce1e38..822392b34 100644 --- a/app/assets/stylesheets/components/banner.scss +++ b/app/assets/stylesheets/components/banner.scss @@ -58,65 +58,60 @@ } -%banner-tip, -.banner-tip { - - @extend %banner; - @include bold-19; - background-color: $yellow; - color: $text-colour; - text-align: left; - margin-top: 0; - - a { - &:link, - &:visited { - color: $text-colour; - text-decoration: underline; - } - } - - ol { - list-style-type: decimal; - } - -} - -.banner-tip-with-tick { - @extend %banner-with-tick; - @extend %banner-tip; - background-image: file-url('tick-black.png'); -} - -.banner-info, -.banner-important { - @extend %banner; - background: $white; - color: $text-colour; - background-image: file-url('icon-important-2x.png'); - background-size: 34px 34px; - background-position: 0 0; - background-repeat: no-repeat; - padding: 7px 0 5px 50px; -} - -.banner-info { - background-image: file-url('icon-information-2x.png'); -} - .banner-mode { @extend %banner; background: $govuk-blue; color: $white; margin-top: $gutter; + padding: $gutter-half; - .heading-medium { + p { margin: 0 0 10px 0; } + &-action { + text-align: right; + } + +} + +.banner-tour { + + @extend %banner; + background: $govuk-blue; + color: $white; + margin-top: $gutter; + padding: $gutter * 2; + + .heading-large { + margin: 0 0 $gutter 0; + } + + p { + + margin-bottom: $gutter; + + &:last-child { + margin-bottom: 0 + } + + & + p { + margin-top: -$gutter-half; + } + + } + a { + @include bold-24; + display: inline-block; + background-image: file-url('tour-next.png'); + background-size: auto 24px; + padding: 0 23px 0 0; + background-position: right 3px; + background-repeat: no-repeat; + &:link, &:visited { color: $white; @@ -124,19 +119,25 @@ &:hover, &:active { - color: $light-blue-25; + background-color: $link-hover-colour; + outline: 10px solid $link-hover-colour; + } + + &:active, + &:focus { + background-color: $yellow; + outline: 10px solid $yellow; } } - .big-number { - - margin-top: 10px; - - &-label { - padding-bottom: 0; - } + img { + max-width: 100%; + display: block; + } + &-image-flush-bottom { + margin: 40px 0 -60px 0; } } diff --git a/app/assets/stylesheets/components/textbox.scss b/app/assets/stylesheets/components/textbox.scss index 703b3b8cf..e6e9e6021 100644 --- a/app/assets/stylesheets/components/textbox.scss +++ b/app/assets/stylesheets/components/textbox.scss @@ -1,6 +1,6 @@ .textbox-highlight { - $tag-background: rgba($light-blue, 0.7); + $tag-background: rgba($light-blue, 0.6); &-wrapper { position: relative; diff --git a/app/assets/stylesheets/views/edit-template.scss b/app/assets/stylesheets/views/edit-template.scss index dd1c8e789..bc20c53ae 100644 --- a/app/assets/stylesheets/views/edit-template.scss +++ b/app/assets/stylesheets/views/edit-template.scss @@ -1,9 +1,23 @@ -.edit-template { +.placeholder-hint { - &-placeholder-hint { - display: block; - padding-top: 20px; - //color: $secondary-text-colour; + display: block; + + &-title { + @include bold-19; } + &-action { + + @include bold-19; + display: inline-block; + text-decoration: underline; + cursor: pointer; + + &:active { + background: $yellow-25; + outline: none; + } + + } + } diff --git a/app/main/__init__.py b/app/main/__init__.py index 3a0af5a11..0d4d793ae 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -23,5 +23,6 @@ from app.main.views import ( api_keys, manage_users, invites, - all_services + all_services, + tour ) diff --git a/app/main/forms.py b/app/main/forms.py index 9153a9427..9d400470b 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -21,7 +21,7 @@ from app.main.validators import (Blacklist, CsvFileValidator, ValidEmailDomainRe def email_address(label='Email address'): return EmailField(label, validators=[ Length(min=5, max=255), - DataRequired(message='Email cannot be empty'), + DataRequired(message='Can’t be empty'), Email(message='Enter a valid email address'), ValidEmailDomainRegex()]) @@ -37,35 +37,35 @@ class UKMobileNumber(TelField): def mobile_number(): return UKMobileNumber('Mobile phone number', - validators=[DataRequired(message='Cannot be empty')]) + validators=[DataRequired(message='Can’t be empty')]) def password(label='Create a password'): return PasswordField(label, - validators=[DataRequired(message='Password can not be empty'), - Length(10, 255, message='Password must be at least 10 characters'), + validators=[DataRequired(message='Can’t be empty'), + Length(10, 255, message='Must be at least 10 characters'), Blacklist(message='That password is blacklisted, too common')]) def sms_code(): verify_code = '^\d{5}$' return StringField('Text message code', - validators=[DataRequired(message='Text message confirmation code can not be empty'), + validators=[DataRequired(message='Can’t be empty'), Regexp(regex=verify_code, - message='Text message confirmation code must be 5 digits')]) + message='Must be 5 digits')]) def email_code(): verify_code = '^\d{5}$' return StringField("Email code", - validators=[DataRequired(message='Email confirmation code can not be empty'), - Regexp(regex=verify_code, message='Email confirmation code must be 5 digits')]) + validators=[DataRequired(message='Can’t be empty'), + Regexp(regex=verify_code, message='Must be 5 digits')]) class LoginForm(Form): email_address = StringField('Email address', validators=[ Length(min=5, max=255), - DataRequired(message='Email cannot be empty'), + DataRequired(message='Can’t be empty'), Email(message='Enter a valid email address') ]) password = PasswordField('Password', validators=[ @@ -76,7 +76,7 @@ class LoginForm(Form): class RegisterUserForm(Form): name = StringField('Full name', - validators=[DataRequired(message='Name can not be empty')]) + validators=[DataRequired(message='Can’t be empty')]) email_address = email_address() mobile_number = mobile_number() password = password() @@ -84,7 +84,7 @@ class RegisterUserForm(Form): class RegisterUserFromInviteForm(Form): name = StringField('Full name', - validators=[DataRequired(message='Name can not be empty')]) + validators=[DataRequired(message='Can’t be empty')]) mobile_number = mobile_number() password = password() service = HiddenField('service') @@ -108,7 +108,7 @@ class InviteUserForm(PermissionsForm): def validate_email_address(self, field): if field.data.lower() == self.invalid_email_address: - raise ValidationError("You can't send an invitation to yourself") + raise ValidationError("You can’t send an invitation to yourself") class TwoFactorForm(Form): @@ -149,7 +149,7 @@ class AddServiceForm(Form): name = StringField( 'Service name', validators=[ - DataRequired(message='Service name can’t be empty') + DataRequired(message='Can’t be empty') ] ) @@ -173,7 +173,7 @@ class ServiceNameForm(Form): name = StringField( u'New name', validators=[ - DataRequired(message='Service name can’t be empty') + DataRequired(message='Can’t be empty') ]) def validate_name(self, a): @@ -199,18 +199,18 @@ class ConfirmPasswordForm(Form): class SMSTemplateForm(Form): name = StringField( u'Template name', - validators=[DataRequired(message="Template name cannot be empty")]) + validators=[DataRequired(message="Can’t be empty")]) template_content = TextAreaField( - u'Message', - validators=[DataRequired(message="Template content cannot be empty")]) + u'Message content', + validators=[DataRequired(message="Can’t be empty")]) class EmailTemplateForm(SMSTemplateForm): subject = StringField( u'Subject', - validators=[DataRequired(message="Subject cannot be empty")]) + validators=[DataRequired(message="Can’t be empty")]) class ForgotPasswordForm(Form): diff --git a/app/main/views/add_service.py b/app/main/views/add_service.py index db9628c78..3ef20c86b 100644 --- a/app/main/views/add_service.py +++ b/app/main/views/add_service.py @@ -44,7 +44,7 @@ def add_service(): user_id=session['user_id'], email_from=email_from) - return redirect(url_for('main.service_dashboard', service_id=service_id)) + return redirect(url_for('main.tour', service_id=service_id, page=1)) else: return render_template( 'views/add-service.html', diff --git a/app/main/views/send.py b/app/main/views/send.py index 6c30a43b1..62125a589 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -77,7 +77,6 @@ def choose_template(service_id, template_type): if template_type not in ['email', 'sms']: abort(404) - jobs = job_api_client.get_job(service_id)['data'] return render_template( 'views/choose-template.html', @@ -91,7 +90,6 @@ def choose_template(service_id, template_type): template_type=template_type, page_heading=get_page_headings(template_type), service=service, - has_jobs=len(jobs), service_id=service_id ) diff --git a/app/main/views/tour.py b/app/main/views/tour.py new file mode 100644 index 000000000..3615b2a9c --- /dev/null +++ b/app/main/views/tour.py @@ -0,0 +1,15 @@ +from flask import render_template +from flask_login import login_required + +from app.main import main + + +@main.route("/services/- Be specific to your service. Remember that there might be - other people in your organisation using GOV.UK Notify. -
- -- Users will see this: -
- -+ To start off with, you can only send messages to yourself. +
++ We can remove these restrictions when you’re ready. +
+ + Next + + {% endcall %} + +{% endblock %} diff --git a/app/templates/views/tour/2.html b/app/templates/views/tour/2.html new file mode 100644 index 000000000..ecad22e6a --- /dev/null +++ b/app/templates/views/tour/2.html @@ -0,0 +1,29 @@ +{% extends "withoutnav_template.html" %} +{% from "components/textbox.html" import textbox %} +{% from "components/page-footer.html" import page_footer %} +{% from "components/banner.html" import banner_wrapper %} + +{% block page_title %} + {{heading}} – GOV.UK Notify +{% endblock %} + +{% block maincolumn_content %} + + {% call banner_wrapper(type='tour') %} ++ Set up a template like this: +
+
+
+
+ Add recipients by uploading a .csv spreadsheet: +
+
+
+
+ Developers, you can add data automatically using an API +
+ + Next + + {% endcall %} + +{% endblock %} diff --git a/app/templates/views/tour/4.html b/app/templates/views/tour/4.html new file mode 100644 index 000000000..f1284d3ef --- /dev/null +++ b/app/templates/views/tour/4.html @@ -0,0 +1,28 @@ +{% extends "withoutnav_template.html" %} +{% from "components/textbox.html" import textbox %} +{% from "components/page-footer.html" import page_footer %} +{% from "components/banner.html" import banner_wrapper %} + +{% block page_title %} + {{heading}} – GOV.UK Notify +{% endblock %} + +{% block maincolumn_content %} + + {% call banner_wrapper(type='tour') %} ++ Notify merges your data with the template and sends the messages +
+ + Next + +
+ {% endcall %}
+
+{% endblock %}
diff --git a/tests/app/main/views/test_add_service.py b/tests/app/main/views/test_add_service.py
index 1da5c1ab5..4162282fc 100644
--- a/tests/app/main/views/test_add_service.py
+++ b/tests/app/main/views/test_add_service.py
@@ -26,7 +26,7 @@ def test_should_add_service_and_redirect_to_next_page(app_,
url_for('main.add_service'),
data={'name': 'testing the post'})
assert response.status_code == 302
- assert response.location == url_for('main.service_dashboard', service_id=101, _external=True)
+ assert response.location == url_for('main.tour', service_id=101, page=1, _external=True)
assert mock_get_services.called
mock_create_service.asset_called_once_with(service_name='testing the post',
active=False,
@@ -44,7 +44,7 @@ def test_should_return_form_errors_when_service_name_is_empty(app_,
client.login(api_user_active, mocker)
response = client.post(url_for('main.add_service'), data={})
assert response.status_code == 200
- assert 'Service name can’t be empty' in response.get_data(as_text=True)
+ assert 'Can’t be empty' in response.get_data(as_text=True)
def test_should_return_form_errors_with_duplicate_service_name_regardless_of_case(app_,
diff --git a/tests/app/main/views/test_manage_users.py b/tests/app/main/views/test_manage_users.py
index 7c264d9e8..70c67dccb 100644
--- a/tests/app/main/views/test_manage_users.py
+++ b/tests/app/main/views/test_manage_users.py
@@ -260,7 +260,7 @@ def test_user_cant_invite_themselves(
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
assert page.h1.string.strip() == 'Invite a team member'
form_error = page.find('span', class_='error-message').string.strip()
- assert form_error == "You can't send an invitation to yourself"
+ assert form_error == "You can’t send an invitation to yourself"
assert not mock_create_invite.called
diff --git a/tests/app/main/views/test_tour.py b/tests/app/main/views/test_tour.py
new file mode 100644
index 000000000..1fe0a0068
--- /dev/null
+++ b/tests/app/main/views/test_tour.py
@@ -0,0 +1,19 @@
+import pytest
+from flask import url_for
+
+import app
+
+
+@pytest.mark.parametrize("page", range(1, 5))
+def test_should_render_tour_pages(
+ app_,
+ api_user_active,
+ mocker,
+ page
+):
+ with app_.test_request_context():
+ with app_.test_client() as client:
+ client.login(api_user_active, mocker)
+ response = client.get(url_for('main.tour', service_id=101, page=page))
+ assert response.status_code == 200
+ assert 'Next' in response.get_data(as_text=True)