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//tour/") +@login_required +def tour(service_id, page): + return render_template( + 'views/tour/{}.html'.format(page), + service_id=service_id, # TODO: fix when Nick’s PR is merged + current_page=page, + next_page=(page + 1) + ) diff --git a/app/templates/views/add-service.html b/app/templates/views/add-service.html index ec03a91e9..05395d5c7 100644 --- a/app/templates/views/add-service.html +++ b/app/templates/views/add-service.html @@ -15,25 +15,6 @@ When people receive notifications, who should they be from? -

- Be specific to your service. Remember that there might be - other people in your organisation using GOV.UK Notify. -

- -

- Users will see this: -

- -
    -
  • - at the start of every text message, eg ‘Vehicle tax: we received your - payment, thank you’ -
  • -
  • - as your email sender name -
  • -
-
{{ textbox(form.name, hint="You can change this later") }} diff --git a/app/templates/views/api-keys.html b/app/templates/views/api-keys.html index b7434514c..b9b5fda72 100644 --- a/app/templates/views/api-keys.html +++ b/app/templates/views/api-keys.html @@ -22,13 +22,6 @@ developer documentation.

- {{ banner( - 'You can only send messages to yourself until you request to go live'.format( - url_for('.service_request_to_go_live', service_id=service_id) - )|safe, - type='important' - ) }} -

Service ID

diff --git a/app/templates/views/choose-template.html b/app/templates/views/choose-template.html index dd0f31503..6bf7674f1 100644 --- a/app/templates/views/choose-template.html +++ b/app/templates/views/choose-template.html @@ -37,17 +37,6 @@ {% endif %}
- {% if not has_jobs %} - {% if current_user.has_permissions(permissions=['send_texts', 'send_emails', 'send_letters'], any_=True) %} - {{ banner( - """ - Send yourself a test - """, - subhead='Next step:', - type="tip" - )}} - {% endif %} - {% endif %}
{% for template in templates %}
diff --git a/app/templates/views/dashboard/dashboard.html b/app/templates/views/dashboard/dashboard.html index 4fe589ed5..bb97d9c7b 100644 --- a/app/templates/views/dashboard/dashboard.html +++ b/app/templates/views/dashboard/dashboard.html @@ -5,26 +5,26 @@ {% endblock %} {% block maincolumn_content %} + + {% if not templates and current_user.has_permissions(['send_texts', 'send_emails', 'send_letters'], any_=True) %} + {% include 'views/dashboard/get-started.html' %} + {% elif service.restricted %} +
+ {% include 'views/dashboard/trial-mode-banner.html' %} +
+ {% endif %} - {% if service.restricted %} - {% include 'views/dashboard/trial-mode-banner.html' %} - {% endif %} - {% if not templates and current_user.has_permissions(['send_texts', 'send_emails', 'send_letters'], any_=True) %} - {% include 'views/dashboard/get-started.html' %} - {% endif %} - - {% if templates %} -
- {% include 'views/dashboard/today.html' %} -
+
+ {% include 'views/dashboard/today.html' %} +
- {% include 'views/dashboard/jobs.html' %} - {% endif %} + {% include 'views/dashboard/jobs.html' %} + {% endblock %} diff --git a/app/templates/views/dashboard/get-started.html b/app/templates/views/dashboard/get-started.html index a2fa514d1..ae91dc2df 100644 --- a/app/templates/views/dashboard/get-started.html +++ b/app/templates/views/dashboard/get-started.html @@ -1,30 +1,23 @@ {% from "components/banner.html" import banner_wrapper %} -

Get started

{% if current_user.has_permissions(['manage_templates']) %} -

- You need to set up a template before you can send messages -

-
    -
  1. - {% call banner_wrapper(type="tip") %} - - Set up a text message template - - {% endcall %} -
  2. -
  3. - {% call banner_wrapper(type="tip") %} - - Set up an email template - - {% endcall %} -
  4. -
+ {% call banner_wrapper(type='tour') %} +

You’re ready to get started

+

+ + Set up a text message template + +

+

+ + Set up an email template + +

+ {% endcall %} {% else %} -

+ {% call banner_wrapper(type='mode') %}

You need to ask your service manager to set up some templates before you can send messages

-

+ {% endcall %} {% endif %} diff --git a/app/templates/views/dashboard/trial-mode-banner.html b/app/templates/views/dashboard/trial-mode-banner.html index 3a3c34829..2265c1d10 100644 --- a/app/templates/views/dashboard/trial-mode-banner.html +++ b/app/templates/views/dashboard/trial-mode-banner.html @@ -2,24 +2,12 @@ {% from "components/big-number.html" import big_number %} {% call banner_wrapper(type="mode") %} - -

Trial mode

-

- We’ll only deliver messages to you and members of your team -
- Find out more -

+ Your service is in trial mode
-
-   -
-
- {{ big_number( - service.limit - statistics.get('emails_requested', 0) - statistics.get('sms_requested', 0), - 'messages left today' - ) }} +
{% endcall %} diff --git a/app/templates/views/edit-email-template.html b/app/templates/views/edit-email-template.html index e49412f6d..c67f6cb1b 100644 --- a/app/templates/views/edit-email-template.html +++ b/app/templates/views/edit-email-template.html @@ -20,23 +20,20 @@
{{ textbox(form.template_content, highlight_tags=True, width='1-1') }} + {{ page_footer( + 'Save', + delete_link=url_for('.delete_service_template', service_id=service_id, template_id=template_id) if template_id or None, + delete_link_text='Delete this template' + ) }}
-
- {{ page_footer( - 'Save', - delete_link=url_for('.delete_service_template', service_id=service_id, template_id=template_id) if template_id or None, - delete_link_text='Delete this template' - ) }} diff --git a/app/templates/views/edit-sms-template.html b/app/templates/views/edit-sms-template.html index be460358d..d2577ce1a 100644 --- a/app/templates/views/edit-sms-template.html +++ b/app/templates/views/edit-sms-template.html @@ -19,23 +19,20 @@
{{ textbox(form.template_content, highlight_tags=True, width='1-1') }} + {{ page_footer( + 'Save', + delete_link=url_for('.delete_service_template', service_id=service_id, template_id=template_id) if template_id or None, + delete_link_text='Delete this template' + ) }}
-
- {{ page_footer( - 'Save', - delete_link=url_for('.delete_service_template', service_id=service_id, template_id=template_id) if template_id or None, - delete_link_text='Delete this template' - ) }} diff --git a/app/templates/views/styleguide.html b/app/templates/views/styleguide.html index 04178f090..2a767e3a9 100644 --- a/app/templates/views/styleguide.html +++ b/app/templates/views/styleguide.html @@ -36,13 +36,6 @@ {{ banner('Are you sure you want to delete?', 'dangerous', delete_button="Yes, delete this thing")}} - {{ banner( - 'Send your first message'|safe, - subhead='Get started', - type='tip' - )}} - - {{ banner('You could go to jail', 'important')}} diff --git a/app/templates/views/tour/1.html b/app/templates/views/tour/1.html new file mode 100644 index 000000000..c25116f6a --- /dev/null +++ b/app/templates/views/tour/1.html @@ -0,0 +1,25 @@ +{% 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') %} +

Trial mode

+

+ 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') %} +

Start with templates

+

+ Set up a template like this: +

+

+ A template for a text message with placeholders for the recipients name, document and date +

+ + Next + + {% endcall %} + +{% endblock %} diff --git a/app/templates/views/tour/3.html b/app/templates/views/tour/3.html new file mode 100644 index 000000000..1cef34c1c --- /dev/null +++ b/app/templates/views/tour/3.html @@ -0,0 +1,32 @@ +{% 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') %} +

Add recipients

+

+ Add recipients by uploading a .csv spreadsheet: +

+

+ A screenshot of a spreadsheet containing data about three people +

+

+ 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') %} +

Send your messages

+

+ 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)