diff --git a/Makefile b/Makefile index 6b6c3554e..5587e362f 100644 --- a/Makefile +++ b/Makefile @@ -23,9 +23,9 @@ CF_API ?= api.cloud.service.gov.uk CF_ORG ?= govuk-notify CF_SPACE ?= ${DEPLOY_ENV} CF_HOME ?= ${HOME} +CF_APP ?= notify-admin $(eval export CF_HOME) -CF_MANIFEST_FILE ?= manifest-${CF_SPACE}.yml NOTIFY_CREDENTIALS ?= ~/.notify-credentials .PHONY: help @@ -195,12 +195,16 @@ cf-login: ## Log in to Cloud Foundry .PHONY: generate-manifest generate-manifest: + $(if ${CF_APP},,$(error Must specify CF_APP)) $(if ${CF_SPACE},,$(error Must specify CF_SPACE)) $(if $(shell which gpg2), $(eval export GPG=gpg2), $(eval export GPG=gpg)) $(if ${GPG_PASSPHRASE_TXT}, $(eval export DECRYPT_CMD=echo -n $$$${GPG_PASSPHRASE_TXT} | ${GPG} --quiet --batch --passphrase-fd 0 --pinentry-mode loopback -d), $(eval export DECRYPT_CMD=${GPG} --quiet --batch -d)) - @./scripts/generate_manifest.py ${CF_MANIFEST_FILE} \ - <(${DECRYPT_CMD} ${NOTIFY_CREDENTIALS}/credentials/${CF_SPACE}/paas/environment-variables.gpg) + @jinja2 --strict manifest.yml.j2 \ + -D environment=${CF_SPACE} \ + -D CF_APP=${CF_APP} \ + --format=yaml \ + <(${DECRYPT_CMD} ${NOTIFY_CREDENTIALS}/credentials/${CF_SPACE}/paas/environment-variables.gpg) 2>&1 .PHONY: cf-deploy cf-deploy: ## Deploys the app to Cloud Foundry @@ -213,18 +217,12 @@ cf-deploy: ## Deploys the app to Cloud Foundry cf delete -f notify-admin-rollback .PHONY: cf-deploy-prototype -cf-deploy-prototype: cf-target ## Deploys the app to Cloud Foundry - $(if ${CF_SPACE},,$(error Must specify CF_SPACE)) - (make -s CF_MANIFEST_FILE=manifest-prototype-${CF_SPACE}.yml generate-manifest) > /tmp/admin_manifest.yml - cf push -f /tmp/admin_manifest.yml - rm /tmp/admin_manifest.yml +cf-deploy-prototype: cf-target ## Deploys the first prototype to Cloud Foundry + cf push -f <(CF_APP=notify-admin-prototype make -s generate-manifest) .PHONY: cf-deploy-prototype-2 -cf-deploy-prototype-2: cf-target ## Deploys the app to Cloud Foundry - $(if ${CF_SPACE},,$(error Must specify CF_SPACE)) - (make -s CF_MANIFEST_FILE=manifest-prototype-2-${CF_SPACE}.yml generate-manifest) > /tmp/admin_manifest.yml - cf push -f /tmp/admin_manifest.yml - rm /tmp/admin_manifest.yml +cf-deploy-prototype-2: cf-target ## Deploys the second prototype to Cloud Foundry + cf push -f <(CF_APP=notify-admin-prototype-2 make -s generate-manifest) .PHONY: cf-rollback cf-rollback: ## Rollbacks the app to the previous release diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..79b31cf9e --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: unset GUNICORN_CMD_ARGS; scripts/run_app_paas.sh gunicorn -c /home/vcap/app/gunicorn_config.py application diff --git a/app/assets/javascripts/updateContent.js b/app/assets/javascripts/updateContent.js index eb94f33eb..928aed1db 100644 --- a/app/assets/javascripts/updateContent.js +++ b/app/assets/javascripts/updateContent.js @@ -2,7 +2,7 @@ "use strict"; var queues = {}; - var dd = new diffDOM.DiffDOM(); + var dd = new diffDOM(); var getRenderer = $component => response => dd.apply( $component.get(0), @@ -34,7 +34,7 @@ ); setTimeout( - () => poll(...arguments), interval + () => poll.apply(window, arguments), interval ); }; diff --git a/app/assets/stylesheets/components/navigation.scss b/app/assets/stylesheets/components/navigation.scss index 388cfffb7..682346b76 100644 --- a/app/assets/stylesheets/components/navigation.scss +++ b/app/assets/stylesheets/components/navigation.scss @@ -108,6 +108,10 @@ a:active { text-decoration: underline; } + + ol ol & { + padding-left: $gutter; + } } &__item--active { diff --git a/app/email_domains.yml b/app/email_domains.yml index cd4ec34fb..bfec593cd 100644 --- a/app/email_domains.yml +++ b/app/email_domains.yml @@ -52,3 +52,5 @@ - caa.co.uk - onevoicewales.wales - cefas.co.uk +- mtvh.co.uk +- officeforstudents.org.uk diff --git a/app/main/views/index.py b/app/main/views/index.py index 40422a93d..aa5d2bc3c 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -65,11 +65,6 @@ def privacy(): return render_template('views/privacy.html') -@main.route('/trial-mode') -def trial_mode(): - return redirect(url_for('.using_notify') + '#trial-mode', 301) - - @main.route('/pricing') def pricing(): return render_template( @@ -85,7 +80,7 @@ def pricing(): @main.route('/delivery-and-failure') def delivery_and_failure(): - return redirect(url_for('.using_notify') + '#messagedeliveryandfailure', 301) + return redirect(url_for('.message_status'), 301) @main.route('/design-patterns-content-guidance') @@ -240,6 +235,30 @@ def roadmap(): ) +@main.route('/features/email') +def features_email(): + return render_template( + 'views/features/emails.html', + navigation_links=features_nav() + ) + + +@main.route('/features/sms') +def features_sms(): + return render_template( + 'views/features/text-messages.html', + navigation_links=features_nav() + ) + + +@main.route('/features/letters') +def features_letters(): + return render_template( + 'views/features/letters.html', + navigation_links=features_nav() + ) + + @main.route('/features/security', endpoint='security') def security(): return render_template( @@ -261,6 +280,27 @@ def using_notify(): return render_template( 'views/using-notify.html', navigation_links=features_nav() + ), 410 + + +@main.route('/features/messages-status') +def message_status(): + return render_template( + 'views/message-status.html', + navigation_links=features_nav() + ) + + +@main.route('/trial-mode') +def trial_mode(): + return redirect(url_for('.trial_mode_new'), 301) + + +@main.route('/features/trial-mode') +def trial_mode_new(): + return render_template( + 'views/trial-mode.html', + navigation_links=features_nav() ) diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index 33c4d782e..c6fa2c76d 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -92,7 +92,7 @@ def view_job(service_id, job_id): version=job['template_version'] )['data'] - just_sent_message = 'Your {} been sent. Printing starts {} at 5.30pm.'.format( + just_sent_message = 'Your {} been sent. Printing starts {} at 5:30pm.'.format( 'letter has' if job['notification_count'] == 1 else 'letters have', printing_today_or_tomorrow() ) diff --git a/app/main/views/notifications.py b/app/main/views/notifications.py index 00765ebbb..30611432e 100644 --- a/app/main/views/notifications.py +++ b/app/main/views/notifications.py @@ -153,7 +153,7 @@ def get_letter_printing_statement(status, created_at): created_at_dt = parser.parse(created_at).replace(tzinfo=None) if letter_can_be_cancelled(status, created_at_dt): - return 'Printing starts {} at 5.30pm'.format(printing_today_or_tomorrow()) + return 'Printing starts {} at 5:30pm'.format(printing_today_or_tomorrow()) else: printed_datetime = utc_string_to_aware_gmt_datetime(created_at) + timedelta(hours=6, minutes=30) printed_date = _format_datetime_short(printed_datetime) diff --git a/app/main/views/sub_navigation_dictionaries.py b/app/main/views/sub_navigation_dictionaries.py index b6ab788bc..e5f7c58ec 100644 --- a/app/main/views/sub_navigation_dictionaries.py +++ b/app/main/views/sub_navigation_dictionaries.py @@ -3,11 +3,33 @@ def features_nav(): { "name": "Features", "link": "main.features", + "sub_navigation_items": [ + { + "name": "Emails", + "link": "main.features_email", + }, + { + "name": "Text messages", + "link": "main.features_sms", + }, + { + "name": "Letters", + "link": "main.features_letters", + }, + ] }, { "name": "Roadmap", "link": "main.roadmap", }, + { + "name": "Trial mode", + "link": "main.trial_mode_new", + }, + { + "name": "Message status", + "link": "main.message_status", + }, { "name": "Security", "link": "main.security", @@ -16,8 +38,4 @@ def features_nav(): "name": "Terms of use", "link": "main.terms", }, - { - "name": "Using Notify", - "link": "main.using_notify", - }, ] diff --git a/app/models/organisation.py b/app/models/organisation.py index c9cdef32b..555bafa44 100644 --- a/app/models/organisation.py +++ b/app/models/organisation.py @@ -88,36 +88,6 @@ class Organisation(JSONModel): 'agreement.'.format(self.name) ) - def as_pricing_paragraph(self, **kwargs): - return Markup(self._as_pricing_paragraph(**kwargs)) - - def _as_pricing_paragraph(self, pricing_link, download_link, support_link, signed_in): - - if not signed_in: - return (( - 'Sign in to download a copy or find ' - 'out if one is already in place with your organisation.' - ).format(pricing_link)) - - if self.agreement_signed is None: - return (( - 'Download the agreement or ' - 'contact us to find out if we already ' - 'have one in place with your organisation.' - ).format(download_link, support_link)) - - return ( - 'Download the agreement ' - '({} {}).'.format( - download_link, - self.name, - { - True: 'has already accepted it', - False: 'hasn’t accepted it yet' - }.get(self.agreement_signed) - ) - ) - @property def _acceptance_required(self): return ( diff --git a/app/models/service.py b/app/models/service.py index a60f1fd5e..f5fa4bb40 100644 --- a/app/models/service.py +++ b/app/models/service.py @@ -114,8 +114,12 @@ class Service(JSONModel): @cached_property def has_team_members(self): - return user_api_client.get_count_of_users_with_permission( - self.id, 'manage_service' + return ( + user_api_client.get_count_of_users_with_permission( + self.id, 'manage_service' + ) + invite_api_client.get_count_of_invites_with_permission( + self.id, 'manage_service' + ) ) > 1 def cancel_invite(self, invited_user_id): diff --git a/app/navigation.py b/app/navigation.py index 33d7d07ad..829a50489 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -49,6 +49,10 @@ class HeaderNavigation(Navigation): }, 'features': { 'features', + 'features_email', + 'features_letters', + 'features_sms', + 'message_status', 'roadmap', 'security', 'terms', @@ -279,6 +283,7 @@ class HeaderNavigation(Navigation): 'template_history', 'template_usage', 'trial_mode', + 'trial_mode_new', 'usage', 'view_job', 'view_job_csv', @@ -451,6 +456,9 @@ class MainNavigation(Navigation): 'email_template', 'error', 'features', + 'features_email', + 'features_letters', + 'features_sms', 'feedback', 'find_users_by_email', 'forgot_password', @@ -469,6 +477,7 @@ class MainNavigation(Navigation): 'letter_branding_preview_image', 'live_services', 'letter_template', + 'message_status', 'manage_org_users', 'new_password', 'old_integration_testing', @@ -525,6 +534,7 @@ class MainNavigation(Navigation): 'thanks', 'triage', 'trial_mode', + 'trial_mode_new', 'trial_services', 'two_factor', 'two_factor_email', @@ -654,6 +664,9 @@ class CaseworkNavigation(Navigation): 'error', 'estimate_usage', 'features', + 'features_email', + 'features_letters', + 'features_sms', 'feedback', 'find_users_by_email', 'forgot_password', @@ -678,6 +691,7 @@ class CaseworkNavigation(Navigation): 'manage_org_users', 'manage_template_folder', 'manage_users', + 'message_status', 'monthly', 'new_password', 'old_integration_testing', @@ -776,6 +790,7 @@ class CaseworkNavigation(Navigation): 'thanks', 'triage', 'trial_mode', + 'trial_mode_new', 'trial_services', 'two_factor', 'two_factor_email', @@ -908,6 +923,9 @@ class OrgNavigation(Navigation): 'error', 'estimate_usage', 'features', + 'features_email', + 'features_letters', + 'features_sms', 'feedback', 'find_users_by_email', 'forgot_password', @@ -930,6 +948,7 @@ class OrgNavigation(Navigation): 'live_services', 'manage_template_folder', 'manage_users', + 'message_status', 'monthly', 'new_password', 'old_integration_testing', @@ -1027,6 +1046,7 @@ class OrgNavigation(Navigation): 'thanks', 'triage', 'trial_mode', + 'trial_mode_new', 'trial_services', 'two_factor', 'two_factor_email', diff --git a/app/notify_client/invite_api_client.py b/app/notify_client/invite_api_client.py index 5cb856713..e07291e24 100644 --- a/app/notify_client/invite_api_client.py +++ b/app/notify_client/invite_api_client.py @@ -1,5 +1,6 @@ from app.models.user import ( InvitedUser, + roles, translate_permissions_from_admin_roles_to_db, ) from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache @@ -44,6 +45,14 @@ class InviteApiClient(NotifyAdminAPIClient): '/service/{}/invite'.format(service_id) )['data'] + def get_count_of_invites_with_permission(self, service_id, permission): + if permission not in roles.keys(): + raise TypeError('{} is not a valid permission'.format(permission)) + return len([ + invited_user for invited_user in self.get_invites_for_service(service_id) + if invited_user.has_permission_for_service(service_id, permission) + ]) + def check_token(self, token): resp = self.get(url='/invite/service/{}'.format(token)) return InvitedUser(**resp['data']) diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html index a78581cfd..2eb67b172 100644 --- a/app/templates/admin_template.html +++ b/app/templates/admin_template.html @@ -86,9 +86,10 @@
diff --git a/app/templates/components/sub-navigation.html b/app/templates/components/sub-navigation.html index ed7464e52..635181f80 100644 --- a/app/templates/components/sub-navigation.html +++ b/app/templates/components/sub-navigation.html @@ -1,18 +1,29 @@ +{% macro sub_navigation_item(item) %} + +{% endmacro %} + {% macro sub_navigation( item_set ) %} diff --git a/app/templates/views/documentation.html b/app/templates/views/documentation.html index 0729b7658..ed834b147 100644 --- a/app/templates/views/documentation.html +++ b/app/templates/views/documentation.html @@ -9,8 +9,10 @@
-

Documentation

-

Integrate with the GOV.UK Notify API using one of our clients (links open in a new tab):

+

Documentation

+

Use the GOV.UK Notify API to send messages automatically.

+

Clients

+

Choose a client to integrate our API with your web application or back-office system. Links to documentation open in a new tab.

    {% for key, label in [ ('java', 'Java'), diff --git a/app/templates/views/features.html b/app/templates/views/features.html index deffa316d..537771192 100644 --- a/app/templates/views/features.html +++ b/app/templates/views/features.html @@ -15,64 +15,72 @@

    Features

    -

    With GOV.UK Notify, you can:

    +

    If you work for central government, a local authority or the NHS, you can use GOV.UK Notify to keep your users updated.

    +

    Notify makes it easy to create, customise and send:

    -

    You don’t need any technical knowledge to use Notify.

    -

    Create an account for free and try it yourself.

    +

    You do not need any technical knowledge to use Notify.

    + {% if not current_user.is_authenticated %} +

    Create an account for free and try it yourself.

    + {% endif %} -

    Create messages quickly and easily

    -

    Without any technical knowledge, you can:

    +

    Reusable message templates

    +

    To send an email, text or letter with Notify, you need to create a reusable message template first.

    +

    Templates let you send the same thing to lots of people, as often as you need to, without writing a new message each time.

    + +

    Personalised content

    +

    Notify makes it easy to send personalised messages from a single template.

    +

    If you want to include personalised content in your message, you can add a placeholder. Placeholders are filled in with details, like a name or reference number, each time you send a message. You can do this manually or upload a list of personal details and let Notify do it for you.

    + +

    Bulk sending

    +

    To send a batch of messages at once, upload a list of contact details to Notify. If you're sending emails and text messages, you can also schedule the date and time you want them to be sent.

    + +

    API integration

    +

    You can integrate the Notify API with your web application or back-office system to send messages automatically.

    +

    Read our API documentation for more information.

    + +

    Reporting

    +

    Notify’s real-time dashboard lets you see the number of messages sent. You can also check the current status of any message to see when it was delivered.

    +

    Read more about how message status works.

    + +

    Permissions

    +

    Control which members of your team can see, create, edit and send messages.

    +

    Notify lets you:

      -
    • create your own message templates
    • -
    • change how messages are formatted
    • -
    • preview messages before you send them
    • +
    • set different permission levels for each team member
    • +
    • invite team members who don’t have a government email address
    • +
    • choose who else can manage team members
    -

    Customise your branding

    -

    You control how your messages look when you send them through Notify. You can:

    -
      -
    • use your organisation’s own branding
    • -
    • choose the name that emails and text messages come from
    • -
    • choose the email address that users reply to
    • -
    • get a unique phone number for text message replies
    • -
    - -

    Send messages in bulk or individually

    -

    With Notify, you can:

    -
      -
    • send bulk messages to lots of users
    • -
    • send personalised messages to individual users
    • -
    • schedule messages to be sent at a particular time
    • -
    • send messages automatically through our API
    • -
    - -

    Track your activity

    -

    Through the Notify dashboard, you can:

    -
      -
    • check the delivery status of any emails and text messages
    • -
    • see a real-time dashboard of your activity
    • -
    • view your usage and reports
    • -
    - -

    Manage your team

    -

    Through the Notify dashboard, you can:

    -
      -
    • set permission levels for different team members
    • -
    • invite new users (including people without a government email address)
    • -
    - -

    Send messages reliably

    +

    Performance

    Notify commits to:

    • sending 95% of emails and text messages within 10 seconds
    • -
    • sending letters by 3pm the next working day (if you submit it through Notify before 5pm)
    • +
    • printing and posting letters by 3pm the next working day (if you send them to us before 5:30pm)
    -

    Notify sends messages through multiple providers. If one provider fails, Notify automatically switches to another so that your messages aren’t affected.

    - -

    You can check how Notify is performing.

    +

    We send messages through several different providers. If one provider fails, Notify switches to another so that your messages are not affected.

    + +

    Visit GOV.UK Performance to see how Notify is performing.

    + +

    Security

    +

    Notify protects and manages data to meet the needs of government services.

    + +

    Hide sensitive information

    +

    Notify lets you redact personal information from your messages after they’ve been sent. This means that only the recipient can see that information.

    + +

    Two-factor authentication

    +

    Notify uses two-factor authentication (2FA) to keep your account secure. When you sign in, we'll send a unique one-time code to your phone and ask you to enter it before we let you use your account.

    +

    Read more about security. + +

    Support

    +

    Notify provides 24-hour online support. If you have an emergency outside office hours, we’ll reply within 30 minutes.

    +

    Find out more about support.

    + +
    +
+ {% endblock %} diff --git a/app/templates/views/features/emails.html b/app/templates/views/features/emails.html new file mode 100644 index 000000000..826647c96 --- /dev/null +++ b/app/templates/views/features/emails.html @@ -0,0 +1,60 @@ +{% extends "withoutnav_template.html" %} +{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %} +{% from "components/sub-navigation.html" import sub_navigation %} + +{% block per_page_title %} + Emails +{% endblock %} + +{% block maincolumn_content %} + +
+
+ {{ sub_navigation(navigation_links) }} +
+
+ +

Emails

+

Send an unlimited number of emails for free with GOV.UK Notify.

+ {% if not current_user.is_authenticated %} +

Create an account and try Notify for yourself.

+ {% endif %} + +

Features

+

Notify makes it easy to:

+
    +
  • create reusable email templates
  • +
  • personalise the content of your emails
  • +
  • send and schedule bulk messages
  • +
+

You can also integrate with our API to send emails automatically.

+ +

Email branding

+

Add your organisation’s logo and brand colour to email templates.

+

You can change your branding in your service settings.

+ +

Send files by email

+

Notify offers a safe and reliable way to send files by email.

+

Upload a file using our API, then send your users an email with a link to download it.

+

Notify uses encrypted links instead of email attachments because:

+
    +
  • they’re more secure
  • + +
  • email attachments are often marked as spam
  • +
+

Contact the team and ask us to turn this feature on for your service.

+

Read our API documentation for more information.

+ +

Add a reply-to address

+

Notify lets you choose the email address that users reply to.

+

Emails with a reply-to address seem more trustworthy and are less likely to be labelled as spam.

+

You can add reply-to addresses in your service settings.

+ +

Pricing

+

It’s free to send emails through Notify.

+

See pricing for more details.

+ +
+
+ +{% endblock %} diff --git a/app/templates/views/features/letters.html b/app/templates/views/features/letters.html new file mode 100644 index 000000000..29244673d --- /dev/null +++ b/app/templates/views/features/letters.html @@ -0,0 +1,56 @@ +{% extends "withoutnav_template.html" %} +{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %} +{% from "components/sub-navigation.html" import sub_navigation %} + +{% block per_page_title %} + Letters +{% endblock %} + +{% block maincolumn_content %} + +
+
+ {{ sub_navigation(navigation_links) }} +
+
+ +

Letters

+

GOV.UK Notify will print, pack and post your letters for you.

+ {% if not current_user.is_authenticated %} +

Create an account and try Notify for yourself.

+ {% endif %} + +

Features

+

Notify makes it easy to:

+
    +
  • create reusable letter templates
  • +
  • personalise the content of your letter
  • +
  • send bulk mail
  • +
+

You can also integrate with our API to send letters automatically.

+ +

Choose your postage

+

Notify can send letters by first or second class post.

+

First class letters are delivered one day after they’re dispatched. Second class letters are delivered 2 days after they’re dispatched.

+

Letters sent before 5:30pm are dispatched the next working day (Monday to Friday). Royal Mail delivers from Monday to Saturday, excluding bank holidays.

+ +

Upload your own letters

+

You can create reusable letter templates in Notify, or upload and send your own letters with the Notify API.

+

Use the letter specification document to help you set up your letter, save it as a PDF, then upload it to Notify.

+

Read our API documentation for more information.

+ +

Pricing

+

It costs between 30p and 76p (plus VAT) to send a letter. Prices include:

+
    +
  • paper
  • +
  • double-sided colour printing
  • +
  • C5 size envelopes with an address window
  • +
  • first or second class postage
  • +
+

Letters can be up to 10 pages long (5 double-sided sheets of paper).

+

See pricing for more details.

+ +
+
+ +{% endblock %} diff --git a/app/templates/views/features/text-messages.html b/app/templates/views/features/text-messages.html new file mode 100644 index 000000000..a85eca875 --- /dev/null +++ b/app/templates/views/features/text-messages.html @@ -0,0 +1,52 @@ +{% extends "withoutnav_template.html" %} +{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %} +{% from "components/sub-navigation.html" import sub_navigation %} + +{% block per_page_title %} + Text messages +{% endblock %} + +{% block maincolumn_content %} + +
+
+ {{ sub_navigation(navigation_links) }} +
+
+ +

Text messages

+

Send thousands of free text messages to UK and international numbers with GOV.UK Notify.

+ {% if not current_user.is_authenticated %} +

Create an account and try Notify for yourself.

+ {% endif %} + +

Features

+

Notify makes it easy to:

+
    +
  • create reusable text message templates
  • +
  • personalise the content of your texts
  • +
  • send and schedule bulk messages
+

You can also integrate with our API to send text messages automatically.

+ +

Receive text messages

+

Let people send messages to your service or reply to your texts.

+

You can see and reply to the messages you receive, or set up your own automated processes to manage replies.

+

Contact the team to request a unique number for text message replies.

+ +

Show people who your texts are from

+

When you send a text message with Notify, the sender name tells people who it's from.

+

You can change the text message sender name from the default of 'GOVUK' in your service settings.

+ +

Pricing

+

Each service you set up has a free annual allowance. After that it costs 1.58 pence (plus VAT) to send a text to a UK number.

+

The free allowance is:

+
    +
  • 250,000 free text messages for central government services
  • +
  • 25,000 free text messages for other public sector services
+

See pricing for more details.

+ +

+
+ + +{% endblock %} diff --git a/app/templates/views/message-status.html b/app/templates/views/message-status.html new file mode 100644 index 000000000..26e168d90 --- /dev/null +++ b/app/templates/views/message-status.html @@ -0,0 +1,106 @@ +{% extends "withoutnav_template.html" %} +{% from "components/sub-navigation.html" import sub_navigation %} +{% from "components/table.html" import mapping_table, row, text_field %} + +{% block per_page_title %} + Message status +{% endblock %} + +{% block maincolumn_content %} + +
+
+ {{ sub_navigation(navigation_links) }} +
+
+ +

Message status

+ +

When you send a message using GOV.UK Notify, it moves through different states.

+

Notify’s real-time dashboard lets you check the status of any message, to see when it was delivered. You can also use our API to check the status of your messages.

+

For security, this information is only available for 7 days after a message has been sent. You can download a report, including a list of sent messages, for your own records.

+ + A picture of the sending flow of messages in Notify, showing the three states of Sending, Delivered and Failed. Also shows the next
+             steps when messages fail, deleting data and trying a new channel for permanent failures, and trying again or trying a new channel for
+             temporary failures +

Types of message status

+

These are the types of message status you’ll see when you’re signed in to Notify.

+

If you’re using our API, some of the statuses you’ll see will be different. Read our documentation for a detailed list of API message statuses.

+ +

Emails

+
+ {% call mapping_table( + caption='Message statuses – emails', + field_headings=['Status', 'Description'], + field_headings_visible=True, + caption_visible=False + ) %} + {% for message_length, charge in [ + ('Sending', 'Notify has sent the message to the provider. The provider will try to deliver the message to the recipient. Notify is waiting for delivery information.'), + ('Delivered', 'The message was successfully delivered. Notify will not tell you if a user has opened or read a message.'), + ('Email address does not exist', 'The provider could not deliver the message because the email address was wrong. You should remove these email addresses from your database.'), + ('Inbox not accepting messages right now', 'The provider could not deliver the message after trying for 72 hours. This can happen when the recipient’s inbox is full. You can try to send the message again.'), + ('Technical failure', 'Your message was not sent because there was a problem between Notify and the provider. You’ll have to try sending your messages again.'), + ] %} + {% call row() %} + {{ text_field(message_length) }} + {{ text_field(charge) }} + {% endcall %} + {% endfor %} + {% endcall %} +
+ +

Text messages

+
+ {% call mapping_table( + caption='Message statuses – text messages', + field_headings=['Status', 'Description'], + field_headings_visible=True, + caption_visible=False + ) %} + {% for message_length, charge in [ + ('Sending', 'Notify has sent the message to the provider. The provider will try to deliver the message to the recipient. Notify is waiting for delivery information.'), + ('Sent internationally', 'The message was sent to an international number. The mobile networks in some countries do not provide any more delivery information.'), + ('Delivered', 'The message was successfully delivered. Notify will not tell you if a user has opened or read a message.'), + ('Phone number does not exist', 'The provider could not deliver the message because the phone number was wrong. You should remove these phone numbers from your database. You’ll still be charged for text messages to numbers that do not exist.'), + ('Phone not accepting messages right now', 'The provider could not deliver the message after trying for 72 hours. This can happen when the recipient’s phone is off. You can try to send the message again. You’ll still be charged for text messages to phones that are not accepting messages.'), + ('Technical failure', 'Your message was not sent because there was a problem between Notify and the provider. You’ll have to try sending your messages again. You will not be charged for text messages that are affected by a technical failure.'), + ] %} + {% call row() %} + {{ text_field(message_length) }} + {{ text_field(charge) }} + {% endcall %} + {% endfor %} + {% endcall %} +
+ +

Letters

+
+ {% call mapping_table( + caption='Message statuses – letters', + field_headings=['Status', 'Description'], + field_headings_visible=True, + caption_visible=False + ) %} + {% for message_length, charge in [ + ('Sent', 'Notify has sent the letter to the provider to be printed.'), + ('Cancelled', 'Sending cancelled. Your letter will not be printed or dispatched.'), + ('Technical failure', 'Notify had an unexpected error while sending the letter to our printing provider.'), + ] %} + {% call row() %} + {{ text_field(message_length) }} + {{ text_field(charge) }} + {% endcall %} + {% endfor %} + {% endcall %} +
+ + +
+
+ +{% endblock %} diff --git a/app/templates/views/pricing.html b/app/templates/views/pricing.html index 3a4538716..e54df2e7d 100644 --- a/app/templates/views/pricing.html +++ b/app/templates/views/pricing.html @@ -14,27 +14,30 @@

Pricing

-

To use GOV.UK Notify, there’s:

+

To use GOV.UK Notify, there’s:

    -
  • no procurement cost
  • no monthly charge
  • -
  • no setup fee to use
  • +
  • no setup fee
  • +
  • no procurement cost
+ {% if not current_user.is_authenticated %} +

Create an account then set up as many different services as you need to. Each service has its own free text message allowance.

+ {% endif %} + +

When you set up a new service it will start in trial mode.

+

Emails

-

It’s free to send emails through GOV.UK Notify.

+

It’s free to send emails through Notify.

Text messages

-

You have a free allowance of text messages each financial year. You’ll get:

+

Each service you set up in Notify has a free annual allowance. The allowance is:

  • 250,000 free text messages for central government services
  • 25,000 free text messages for other public sector services

It costs 1.58 pence (plus VAT) for each text message you send after your free allowance.

- -

Multiple services

-

If your organisation offers multiple services, you can have a different Notify account for each of them.

-

Each service will get its own free message allowance.

+

See how to pay.

Long text messages

If a text message is beyond a certain length, it’ll be charged as more than one message:

@@ -89,11 +92,11 @@

Letters

-

The cost of sending a letter depends on how many sheets of paper you need and the class of postage.

+

The cost of sending a letter depends on the postage you choose and how many sheets of paper you need.

{% call mapping_table( caption='Letter pricing', - field_headings=['', 'Second class', 'First class'], + field_headings=['Paper', 'Second class', 'First class'], field_headings_visible=True, caption_visible=False ) %} @@ -112,24 +115,24 @@ {% endfor %} {% endcall %}
-

Letter prices include:

+

Prices include:

    -
  • paper
  • -
  • double-sided colour printing
  • -
  • envelopes
  • -
  • postage
  • +
  • paper
  • +
  • double-sided colour printing
  • +
  • C5 size envelopes with an address window
  • +
  • first or second class postage

How to pay

-

You can find details of how to pay for Notify in our data sharing and financial agreement.

-

- {{ current_user.default_organisation.as_pricing_paragraph( - pricing_link=url_for('main.sign_in', next=url_for('main.pricing', _anchor='paying')), - download_link=url_for('main.agreement'), - support_link=url_for('.feedback', ticket_type='ask-question-give-feedback', body='agreement'), - signed_in=current_user.is_authenticated, - ) }} -

+

Before you can use Notify to send letters or pay for text messages you’ll need to:

+
    +
  • send us a request to make your service live
  • +
  • sign our data sharing and financial agreement
  • +
  • raise a Purchase Order (PO)
  • +
+ +

You may need to set up the Cabinet Office as a supplier before you can raise a PO.

+

After you’ve done that, Notify will send you an invoice at the end of each quarter. If the value of an invoice is very small, we roll it into the next quarter to save time and effort.

diff --git a/app/templates/views/support/index.html b/app/templates/views/support/index.html index 7c909aff5..f2738d5f9 100644 --- a/app/templates/views/support/index.html +++ b/app/templates/views/support/index.html @@ -13,30 +13,29 @@

Support

+

We provide 24-hour online support for teams with a live service on GOV.UK Notify.

{% call form_wrapper(class="bottom-gutter-2") %} {{ radios(form.support_type) }} {{ page_footer('Continue') }} {% endcall %} -

If something’s wrong, you can check the GOV.UK Notify system status page to see if we’re already aware of it.

- -

24-hour support

-

You can get 24-hour online support for Notify if you have a live account.

-

During office hours

-

Our office hours are 9.30am to 5.30pm, Monday to Friday.

-

Contact us using one of the options on this page and you’ll get a reply within 30 minutes.

-

You can also get in touch with us on Slack.

-

Out-of-hours

-

Outside office hours, response times depend on whether you’re reporting an emergency or not.

+

You can also get in touch with us on Slack.

+ +

Office hours

+

Our office hours are 9:30am to 5:30pm, Monday to Friday.

+

When you report a problem in office hours, we’ll aim to read it within 30 minutes and reply within one working day.

+ +

Out-of-hours

+

Outside office hours, response times depend on whether you’re reporting an emergency.

If it’s an emergency, we’ll reply within 30 minutes and update you every hour until the problem’s fixed.

+

If your problem is not an emergency, we’ll reply within one working day.

A problem is only classed as an emergency if:

-

We’ll reply during the next working day for any other problems.

diff --git a/app/templates/views/trial-mode.html b/app/templates/views/trial-mode.html new file mode 100644 index 000000000..1f4321d76 --- /dev/null +++ b/app/templates/views/trial-mode.html @@ -0,0 +1,45 @@ +{% extends "withoutnav_template.html" %} +{% from "components/sub-navigation.html" import sub_navigation %} + +{% block per_page_title %} + Using Notify +{% endblock %} + +{% block maincolumn_content %} + +
+
+ {{ sub_navigation(navigation_links) }} +
+
+ +

Trial mode

+ +

When you set up a new service it will start in trial mode. This lets you try out GOV.UK Notify, with a few restrictions.

+

A service in trial mode can only:

+ + +

+ To remove these restrictions, you can + {% if current_service and current_service.trial_mode %} + send us a request to go live. + {% else %} + send us a request to go live. + {% endif %} +

+ +

Before you can request to go live, you must:

+ + + +
+ +{% endblock %} diff --git a/app/templates/views/using-notify.html b/app/templates/views/using-notify.html index e6753719d..f986accf4 100644 --- a/app/templates/views/using-notify.html +++ b/app/templates/views/using-notify.html @@ -15,27 +15,48 @@

Using Notify

-

Trial mode

-

When you create a GOV.UK Notify account, you’ll start in trial mode. This lets you try out the service, but has some restrictions in place.

+

The information on this page has moved.

+ +

Find out more about:

+ + +
diff --git a/domains.py b/domains.py deleted file mode 100755 index 3cb1245a8..000000000 --- a/domains.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import os -import yaml -from itertools import chain -from operator import itemgetter -from sys import argv -from app.utils import AgreementInfo - -_dir_path = os.path.dirname(os.path.realpath(__file__)) - - -if len(argv) < 2: - raise TypeError('Must specify `orgs` or `domains` as the first argument to this script') - - -with open('{}/app/domains.yml'.format(_dir_path)) as source: - - data = yaml.load(source) - - for domain, details in data.items(): - if isinstance(details, dict): - # We’re looking at the canonical domain - data[domain]['domains'] = [domain] - - for domain, details in data.items(): - if isinstance(details, str): - # This is an alias, let’s add it to the canonical domain - data[AgreementInfo(domain).canonical_domain]['domains'].append(domain) - - out_data = [ - details for domain, details in data.items() - if isinstance(details, dict) - ] - - if argv[1] == 'orgs': - print(yaml.dump(out_data)) # noqa - elif argv[1] == 'domains': - print( # noqa - sorted( - set( - chain.from_iterable( - map( - itemgetter('domains'), out_data - ) - ) - ) - ) - ) - else: - raise TypeError('Must specify `orgs` or `domains` as the first argument to this script') diff --git a/gulpfile.js b/gulpfile.js index 60533475f..15df27cb6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -105,7 +105,7 @@ const javascripts = () => { paths.npm + 'hogan.js/dist/hogan-3.0.2.js', paths.npm + 'jquery/dist/jquery.min.js', paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js', - paths.npm + 'diff-dom/browser/diffDOM.js', + paths.npm + 'diff-dom/diffDOM.js', paths.npm + 'timeago/jquery.timeago.js', paths.npm + 'textarea-caret/index.js' ])) diff --git a/manifest-base.yml b/manifest-base.yml deleted file mode 100644 index c28457046..000000000 --- a/manifest-base.yml +++ /dev/null @@ -1,46 +0,0 @@ ---- - -buildpack: python_buildpack -command: > - unset GUNICORN_CMD_ARGS; - scripts/run_app_paas.sh - gunicorn - -c /home/vcap/app/gunicorn_config.py - application - -instances: 1 -memory: 1G - -services: - - logit-ssl-syslog-drain - -env: - NOTIFY_APP_NAME: admin - - # Credentials variables - ADMIN_CLIENT_SECRET: null - ADMIN_BASE_URL: null - API_HOST_NAME: null - DANGEROUS_SALT: null - SECRET_KEY: null - ROUTE_SECRET_KEY_1: null - ROUTE_SECRET_KEY_2: null - - AWS_ACCESS_KEY_ID: null - AWS_SECRET_ACCESS_KEY: null - - ANTIVIRUS_API_HOST: null - ANTIVIRUS_API_KEY: null - - STATSD_PREFIX: null - - ZENDESK_API_KEY: null - - TEMPLATE_PREVIEW_API_HOST: null - TEMPLATE_PREVIEW_API_KEY: null - - REDIS_ENABLED: null - REDIS_URL: null - -applications: - - name: notify-admin diff --git a/manifest-preview.yml b/manifest-preview.yml deleted file mode 100644 index a8f10e896..000000000 --- a/manifest-preview.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- - -inherit: manifest-base.yml - -routes: - - route: notify-admin-preview.cloudapps.digital - - route: www.notify.works diff --git a/manifest-production.yml b/manifest-production.yml deleted file mode 100644 index 1eca336c7..000000000 --- a/manifest-production.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- - -inherit: manifest-base.yml - -routes: - - route: notify-admin-production.cloudapps.digital - - route: www.notifications.service.gov.uk -instances: 2 -memory: 1G diff --git a/manifest-prototype-2-base.yml b/manifest-prototype-2-base.yml deleted file mode 100644 index 284fb6427..000000000 --- a/manifest-prototype-2-base.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- - -buildpack: python_buildpack -command: scripts/run_app_paas.sh gunicorn -w 5 -b 0.0.0.0:$PORT application -instances: 1 -memory: 1G -env: - NOTIFY_APP_NAME: admin - - # Credentials variables - ADMIN_CLIENT_SECRET: null - ADMIN_BASE_URL: null - API_HOST_NAME: null - DANGEROUS_SALT: null - SECRET_KEY: null - ROUTE_SECRET_KEY_1: null - ROUTE_SECRET_KEY_2: null - - AWS_ACCESS_KEY_ID: null - AWS_SECRET_ACCESS_KEY: null - - STATSD_PREFIX: null - - ZENDESK_API_KEY: null - - TEMPLATE_PREVIEW_API_HOST: null - TEMPLATE_PREVIEW_API_KEY: null - -applications: - - name: notify-admin-prototype-2 diff --git a/manifest-prototype-2-preview.yml b/manifest-prototype-2-preview.yml deleted file mode 100644 index 1a998d2d9..000000000 --- a/manifest-prototype-2-preview.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- - -inherit: manifest-prototype-2-base.yml - -routes: - - route: notify-admin-prototype-2-preview.cloudapps.digital diff --git a/manifest-prototype-2-production.yml b/manifest-prototype-2-production.yml deleted file mode 100644 index ffffa7686..000000000 --- a/manifest-prototype-2-production.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- - -inherit: manifest-prototype-2-base.yml - -routes: - - route: notify-admin-prototype-2-production.cloudapps.digital diff --git a/manifest-prototype-base.yml b/manifest-prototype-base.yml deleted file mode 100644 index d2165800e..000000000 --- a/manifest-prototype-base.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- - -buildpack: python_buildpack -command: scripts/run_app_paas.sh gunicorn -w 5 -b 0.0.0.0:$PORT application -instances: 1 -memory: 1G -env: - NOTIFY_APP_NAME: admin - - # Credentials variables - ADMIN_CLIENT_SECRET: null - ADMIN_BASE_URL: null - API_HOST_NAME: null - DANGEROUS_SALT: null - SECRET_KEY: null - ROUTE_SECRET_KEY_1: null - ROUTE_SECRET_KEY_2: null - - AWS_ACCESS_KEY_ID: null - AWS_SECRET_ACCESS_KEY: null - - STATSD_PREFIX: null - - ZENDESK_API_KEY: null - - TEMPLATE_PREVIEW_API_HOST: null - TEMPLATE_PREVIEW_API_KEY: null - -applications: - - name: notify-admin-prototype diff --git a/manifest-prototype-preview.yml b/manifest-prototype-preview.yml deleted file mode 100644 index 140d17a98..000000000 --- a/manifest-prototype-preview.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- - -inherit: manifest-prototype-base.yml - -routes: - - route: notify-admin-prototype-preview.cloudapps.digital diff --git a/manifest-prototype-production.yml b/manifest-prototype-production.yml deleted file mode 100644 index 4fc239945..000000000 --- a/manifest-prototype-production.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- - -inherit: manifest-prototype-base.yml - -routes: - - route: notify-admin-prototype-production.cloudapps.digital diff --git a/manifest-prototype-staging.yml b/manifest-prototype-staging.yml deleted file mode 100644 index 0c840b43f..000000000 --- a/manifest-prototype-staging.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- - -inherit: manifest-prototype-base.yml - -routes: - - route: notify-admin-prototype-staging.cloudapps.digital diff --git a/manifest-sandbox.yml b/manifest-sandbox.yml deleted file mode 100644 index 8f22b1e17..000000000 --- a/manifest-sandbox.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- - -inherit: manifest-base.yml - -routes: - - route: notify-admin-sandbox.cloudapps.digital diff --git a/manifest-staging.yml b/manifest-staging.yml deleted file mode 100644 index c4a719da0..000000000 --- a/manifest-staging.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- - -inherit: manifest-base.yml - -routes: - - route: notify-admin-staging.cloudapps.digital - - route: www.staging-notify.works - -instances: 2 -memory: 1G diff --git a/manifest.yml.j2 b/manifest.yml.j2 new file mode 100644 index 000000000..ccf758095 --- /dev/null +++ b/manifest.yml.j2 @@ -0,0 +1,57 @@ +{%- set apps = { + 'notify-admin': { + 'routes': { + 'preview': ['www.notify.works'], + 'staging': ['www.staging-notify.works'], + 'production': ['www.notifications.service.gov.uk'], + } + }, + 'notify-admin-prototype': {}, + 'notify-admin-prototype-2': {} +} -%} + +{%- set app = apps[CF_APP] -%} + +--- +applications: + - name: {{ CF_APP }} + buildpack: python_buildpack + + memory: 1G + + routes: + - route: {{ CF_APP }}-{{ environment }}.cloudapps.digital + {%- for route in app.get('routes', {}).get(environment, []) %} + - route: {{ route }} + {%- endfor %} + + services: + - logit-ssl-syslog-drain + + env: + NOTIFY_APP_NAME: admin + + # Credentials variables + ADMIN_CLIENT_SECRET: '{{ ADMIN_CLIENT_SECRET }}' + ADMIN_BASE_URL: '{{ ADMIN_BASE_URL }}' + API_HOST_NAME: '{{ API_HOST_NAME }}' + DANGEROUS_SALT: '{{ DANGEROUS_SALT }}' + SECRET_KEY: '{{ SECRET_KEY }}' + ROUTE_SECRET_KEY_1: '{{ ROUTE_SECRET_KEY_1 }}' + ROUTE_SECRET_KEY_2: '{{ ROUTE_SECRET_KEY_2 }}' + + AWS_ACCESS_KEY_ID: '{{ AWS_ACCESS_KEY_ID }}' + AWS_SECRET_ACCESS_KEY: '{{ AWS_SECRET_ACCESS_KEY }}' + + ANTIVIRUS_API_HOST: '{{ ANTIVIRUS_API_HOST }}' + ANTIVIRUS_API_KEY: '{{ ANTIVIRUS_API_KEY }}' + + STATSD_PREFIX: '{{ STATSD_PREFIX }}' + + ZENDESK_API_KEY: '{{ ZENDESK_API_KEY }}' + + TEMPLATE_PREVIEW_API_HOST: '{{ TEMPLATE_PREVIEW_API_HOST }}' + TEMPLATE_PREVIEW_API_KEY: '{{ TEMPLATE_PREVIEW_API_KEY }}' + + REDIS_ENABLED: '{{ REDIS_ENABLED }}' + REDIS_URL: '{{ REDIS_URL }}' diff --git a/package.json b/package.json index 2273a1e9a..be991f89a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "dependencies": { "@babel/core": "7.4.0", "@babel/preset-env": "7.4.2", - "diff-dom": "https://github.com/fiduswriter/diffDOM/archive/v3.1.0.tar.gz", + "diff-dom": "2.5.1", "govuk-elements-sass": "3.1.2", "govuk_frontend_toolkit": "8.1.0", "govuk_template_jinja": "0.24.1", @@ -45,8 +45,5 @@ "gulp-sass-lint": "1.4.0", "jshint": "2.10.2", "jshint-stylish": "2.2.1" - }, - "peerDependencies": { - "rollup": "1.10.0" } } diff --git a/requirements-app.txt b/requirements-app.txt index a4ca59d71..a95d82259 100644 --- a/requirements-app.txt +++ b/requirements-app.txt @@ -8,11 +8,11 @@ Flask-Login==0.4.1 blinker==1.4 pyexcel==0.5.13 -pyexcel-io==0.5.16 +pyexcel-io==0.5.17 pyexcel-xls==0.5.8 pyexcel-xlsx==0.5.7 pyexcel-ods3==0.5.3 -pytz==2018.9 +pytz==2019.1 gunicorn==19.7.1 # pyup: ignore, >19.8 breaks eventlet patching eventlet==0.24.1 notifications-python-client==5.3.0 diff --git a/requirements.txt b/requirements.txt index e2bb6204d..510d59b95 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,11 +10,11 @@ Flask-Login==0.4.1 blinker==1.4 pyexcel==0.5.13 -pyexcel-io==0.5.16 +pyexcel-io==0.5.17 pyexcel-xls==0.5.8 pyexcel-xlsx==0.5.7 pyexcel-ods3==0.5.3 -pytz==2018.9 +pytz==2019.1 gunicorn==19.7.1 # pyup: ignore, >19.8 breaks eventlet patching eventlet==0.24.1 notifications-python-client==5.3.0 @@ -28,10 +28,10 @@ itsdangerous==0.24 # pyup: <1.0.0 git+https://github.com/alphagov/notifications-utils.git@31.2.4#egg=notifications-utils==31.2.4 ## The following requirements were added by pip freeze: -awscli==1.16.136 +awscli==1.16.140 bleach==3.1.0 boto3==1.6.16 -botocore==1.12.126 +botocore==1.12.130 certifi==2019.3.9 chardet==3.0.4 Click==7.0 @@ -45,7 +45,7 @@ future==0.17.1 greenlet==0.4.15 idna==2.8 jdcal==1.4 -Jinja2==2.10 +Jinja2==2.10.1 jmespath==0.9.4 lml==0.0.9 lxml==4.3.3 @@ -72,7 +72,7 @@ statsd==3.3.0 texttable==1.6.1 urllib3==1.24.1 webencodings==0.5.1 -Werkzeug==0.15.1 +Werkzeug==0.15.2 WTForms==2.2.1 xlrd==1.2.0 xlwt==1.3.0 diff --git a/requirements_for_test.txt b/requirements_for_test.txt index 266092efd..3b0140c52 100644 --- a/requirements_for_test.txt +++ b/requirements_for_test.txt @@ -11,3 +11,5 @@ freezegun==0.3.11 flake8==3.7.7 flake8-print==3.1.0 requests-mock==1.5.2 +# used for creating manifest file locally +jinja2-cli[yaml]==0.6.0 diff --git a/runtime.txt b/runtime.txt index f0e4f4e5d..454452e35 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.5.5 +python-3.5.x diff --git a/scripts/generate_manifest.py b/scripts/generate_manifest.py deleted file mode 100755 index ce7982763..000000000 --- a/scripts/generate_manifest.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import os -import sys - -import json -import yaml - - -def merge_dicts(a, b): - if not (isinstance(a, dict) and isinstance(b, dict)): - raise ValueError("Error merging variables: '{}' and '{}'".format( - type(a).__name__, type(b).__name__ - )) - - result = a.copy() - for key, val in b.items(): - if isinstance(result.get(key), dict): - result[key] = merge_dicts(a[key], b[key]) - else: - result[key] = val - - return result - - -def load_manifest(manifest_file): - with open(manifest_file) as f: - manifest = yaml.load(f) - - if 'inherit' in manifest: - inherit_file = os.path.join(os.path.dirname(manifest_file), manifest.pop('inherit')) - manifest = merge_dicts(load_manifest(inherit_file), manifest) - - return manifest - - -def load_variables(vars_files): - variables = {} - for vars_file in vars_files: - with open(vars_file) as f: - variables = merge_dicts(variables, yaml.load(f)) - - return { - k.upper(): json.dumps(v) if isinstance(v, (dict, list)) else v - for k, v in variables.items() - } - - -def paas_manifest(manifest_file, *vars_files): - manifest = load_manifest(manifest_file) - variables = load_variables(vars_files) - - for key in manifest.get('env', {}): - if key in variables: - manifest['env'][key] = variables[key] - - return yaml.dump(manifest, default_flow_style=False, allow_unicode=True) - - -if __name__ == "__main__": - print('---') # noqa - print(paas_manifest(*sys.argv[1:])) # noqa diff --git a/tests/app/main/views/test_index.py b/tests/app/main/views/test_index.py index e712d174c..3d3088bd2 100644 --- a/tests/app/main/views/test_index.py +++ b/tests/app/main/views/test_index.py @@ -77,8 +77,10 @@ def test_robots(client): @pytest.mark.parametrize('view', [ - 'cookies', 'privacy', 'using_notify', 'pricing', 'terms', 'roadmap', - 'features', 'callbacks', 'documentation', 'security' + 'cookies', 'privacy', 'pricing', 'terms', 'roadmap', + 'features', 'callbacks', 'documentation', 'security', + 'message_status', 'features_email', 'features_sms', + 'features_letters', ]) def test_static_pages( client_request, @@ -89,26 +91,6 @@ def test_static_pages( assert not page.select_one('meta[name=description]') -@pytest.mark.parametrize('view, expected_anchor', [ - ('delivery_and_failure', 'messagedeliveryandfailure'), - ('trial_mode', 'trial-mode'), -]) -def test_old_static_pages_redirect_to_using_notify_with_anchor( - client_request, - view, - expected_anchor, -): - client_request.get( - 'main.{}'.format(view), - _expected_status=301, - _expected_redirect=url_for( - 'main.using_notify', - _anchor=expected_anchor, - _external=True - ), - ) - - @pytest.mark.parametrize('view, expected_view', [ ('information_risk_management', 'security'), ('old_integration_testing', 'integration_testing'), @@ -117,6 +99,7 @@ def test_old_static_pages_redirect_to_using_notify_with_anchor( ('old_terms', 'terms'), ('information_security', 'using_notify'), ('old_using_notify', 'using_notify'), + ('delivery_and_failure', 'message_status'), ]) def test_old_static_pages_redirect( client, @@ -131,6 +114,10 @@ def test_old_static_pages_redirect( ) +def test_old_using_notify_page(client_request): + client_request.get('main.using_notify', _expected_status=410) + + def test_old_integration_testing_page( client_request, ): @@ -162,29 +149,11 @@ def test_terms_is_generic_if_user_is_not_logged_in( ) -def test_pricing_is_generic_if_user_is_not_logged_in( - client -): - response = client.get(url_for('main.pricing')) - assert response.status_code == 200 - page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') - last_paragraph = page.select('main p')[-1] - assert normalize_spaces(last_paragraph.text) == ( - 'Sign in to download a copy or find out if one is already ' - 'in place with your organisation.' - ) - assert last_paragraph.select_one('a')['href'] == url_for( - 'main.sign_in', - next=url_for('main.pricing', _anchor='paying'), - ) - - @pytest.mark.parametrize(( 'name,' 'agreement_signed,' 'expected_terms_paragraph,' 'expected_terms_link,' - 'expected_pricing_paragraph' ), [ ( 'Cabinet Office', @@ -194,10 +163,6 @@ def test_pricing_is_generic_if_user_is_not_logged_in( 'the GOV.UK Notify data sharing and financial agreement.' ), None, - ( - 'Download the agreement ' - '(Cabinet Office has already accepted it).' - ), ), ( 'Aylesbury Town Council', @@ -211,10 +176,6 @@ def test_pricing_is_generic_if_user_is_not_logged_in( url_for, 'main.agreement', ), - ( - 'Download the agreement ' - '(Aylesbury Town Council hasn’t accepted it yet).' - ), ), ( None, @@ -229,10 +190,6 @@ def test_pricing_is_generic_if_user_is_not_logged_in( url_for, 'main.agreement', ), - ( - 'Download the agreement or contact us to find out if ' - 'we already have one in place with your organisation.' - ), ), ( 'Met Office', @@ -245,10 +202,6 @@ def test_pricing_is_generic_if_user_is_not_logged_in( url_for, 'main.agreement', ), - ( - 'Download the agreement (Met Office hasn’t accepted it ' - 'yet).' - ), ), ]) def test_terms_tells_logged_in_users_what_we_know_about_their_agreement( @@ -259,7 +212,6 @@ def test_terms_tells_logged_in_users_what_we_know_about_their_agreement( agreement_signed, expected_terms_paragraph, expected_terms_link, - expected_pricing_paragraph, ): mock_get_organisation_by_domain( mocker, @@ -267,13 +219,12 @@ def test_terms_tells_logged_in_users_what_we_know_about_their_agreement( agreement_signed=agreement_signed, ) terms_page = client_request.get('main.terms') - pricing_page = client_request.get('main.pricing') + assert normalize_spaces(terms_page.select('main p')[1].text) == expected_terms_paragraph if expected_terms_link: assert terms_page.select_one('main p a')['href'] == expected_terms_link() else: assert not terms_page.select_one('main p').select('a') - assert normalize_spaces(pricing_page.select('main p')[-1].text) == expected_pricing_paragraph def test_css_is_served_from_correct_path(client_request): diff --git a/tests/app/main/views/test_jobs.py b/tests/app/main/views/test_jobs.py index a562cd33a..9d145a60e 100644 --- a/tests/app/main/views/test_jobs.py +++ b/tests/app/main/views/test_jobs.py @@ -360,7 +360,7 @@ def test_should_show_letter_job_with_banner_after_sending_before_1730( assert page.select('p.bottom-gutter') == [] assert normalize_spaces(page.select('.banner-default-with-tick')[0].text) == ( - 'Your letter has been sent. Printing starts today at 5.30pm.' + 'Your letter has been sent. Printing starts today at 5:30pm.' ) @@ -383,7 +383,7 @@ def test_should_show_letter_job_with_banner_when_there_are_multiple_CSV_rows( assert page.select('p.bottom-gutter') == [] assert normalize_spaces(page.select('.banner-default-with-tick')[0].text) == ( - 'Your letters have been sent. Printing starts today at 5.30pm.' + 'Your letters have been sent. Printing starts today at 5:30pm.' ) @@ -406,7 +406,7 @@ def test_should_show_letter_job_with_banner_after_sending_after_1730( assert page.select('p.bottom-gutter') == [] assert normalize_spaces(page.select('.banner-default-with-tick')[0].text) == ( - 'Your letter has been sent. Printing starts tomorrow at 5.30pm.' + 'Your letter has been sent. Printing starts tomorrow at 5:30pm.' ) diff --git a/tests/app/main/views/test_notifications.py b/tests/app/main/views/test_notifications.py index 2a99eb110..b856924ba 100644 --- a/tests/app/main/views/test_notifications.py +++ b/tests/app/main/views/test_notifications.py @@ -167,7 +167,7 @@ def test_notification_page_shows_page_for_letter_notification( "‘sample template’ was sent by Test User on 1 January at 1:01am" ) assert normalize_spaces(page.select('main p:nth-of-type(2)')[0].text) == ( - 'Printing starts today at 5.30pm' + 'Printing starts today at 5:30pm' ) assert normalize_spaces(page.select('main p:nth-of-type(3)')[0].text) == ( 'Estimated delivery date: 6 January' @@ -416,7 +416,7 @@ def test_notification_page_shows_page_for_first_class_letter_notification( notification_id=fake_uuid, ) - assert normalize_spaces(page.select('main p:nth-of-type(2)')[0].text) == 'Printing starts tomorrow at 5.30pm' + assert normalize_spaces(page.select('main p:nth-of-type(2)')[0].text) == 'Printing starts tomorrow at 5:30pm' assert normalize_spaces(page.select('main p:nth-of-type(3)')[0].text) == 'Estimated delivery date: 5 January' assert normalize_spaces(page.select_one('.letter-postage').text) == ( 'Postage: first class' @@ -716,14 +716,14 @@ def test_should_show_image_of_precompiled_letter_notification( @pytest.mark.parametrize('created_at, current_datetime', [ ('2017-07-07T12:00:00+00:00', '2017-07-07 16:29:00'), # created today, summer ('2017-12-12T12:00:00+00:00', '2017-12-12 17:29:00'), # created today, winter - ('2017-12-12T21:30:00+00:00', '2017-12-13 17:29:00'), # created after 5.30 yesterday + ('2017-12-12T21:30:00+00:00', '2017-12-13 17:29:00'), # created after 5:30 yesterday ('2017-03-25T17:30:00+00:00', '2017-03-26 16:29:00'), # over clock change period on 2017-03-26 ]) def test_get_letter_printing_statement_when_letter_prints_today(created_at, current_datetime): with freeze_time(current_datetime): statement = get_letter_printing_statement('created', created_at) - assert statement == 'Printing starts today at 5.30pm' + assert statement == 'Printing starts today at 5:30pm' @pytest.mark.parametrize('created_at, current_datetime', [ @@ -734,7 +734,7 @@ def test_get_letter_printing_statement_when_letter_prints_tomorrow(created_at, c with freeze_time(current_datetime): statement = get_letter_printing_statement('created', created_at) - assert statement == 'Printing starts tomorrow at 5.30pm' + assert statement == 'Printing starts tomorrow at 5:30pm' @pytest.mark.parametrize('created_at, print_day', [ diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index bb81f4925..2919689d0 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -643,6 +643,7 @@ def test_should_check_if_estimated_volumes_provided( mock_get_service_templates, mock_get_users_by_service, mock_get_service_organisation, + mock_get_invites_for_service, volumes, consent_to_research, expected_estimated_volumes_item, @@ -675,9 +676,14 @@ def test_should_check_if_estimated_volumes_provided( ) -@pytest.mark.parametrize('count_of_users_with_manage_service, expected_user_checklist_item', [ - (1, 'Add a team member who can manage settings, team and usage Not completed'), - (2, 'Add a team member who can manage settings, team and usage Completed'), +@pytest.mark.parametrize(( + 'count_of_users_with_manage_service,' + 'count_of_invites_with_manage_service,' + 'expected_user_checklist_item' +), [ + (1, 0, 'Add a team member who can manage settings, team and usage Not completed'), + (2, 0, 'Add a team member who can manage settings, team and usage Completed'), + (1, 1, 'Add a team member who can manage settings, team and usage Completed'), ]) @pytest.mark.parametrize('count_of_templates, expected_templates_checklist_item', [ (0, 'Add templates with examples of the content you plan to send Not completed'), @@ -705,6 +711,7 @@ def test_should_check_for_sending_things_right( mock_get_service_organisation, single_sms_sender, count_of_users_with_manage_service, + count_of_invites_with_manage_service, expected_user_checklist_item, count_of_templates, expected_templates_checklist_item, @@ -720,9 +727,13 @@ def test_should_check_for_sending_things_right( }.get(template_type) mock_count_users = mocker.patch( - 'app.main.views.service_settings.user_api_client.get_count_of_users_with_permission', + 'app.models.service.user_api_client.get_count_of_users_with_permission', return_value=count_of_users_with_manage_service ) + mock_count_invites = mocker.patch( + 'app.models.service.invite_api_client.get_count_of_invites_with_permission', + return_value=count_of_invites_with_manage_service + ) mock_templates = mocker.patch( 'app.models.service.Service.all_templates', @@ -760,6 +771,7 @@ def test_should_check_for_sending_things_right( assert normalize_spaces(checklist_items[3].text) == expected_reply_to_checklist_item mock_count_users.assert_called_once_with(SERVICE_ONE_ID, 'manage_service') + mock_count_invites.assert_called_once_with(SERVICE_ONE_ID, 'manage_service') assert mock_templates.called is True if count_of_email_templates: @@ -779,6 +791,7 @@ def test_should_not_show_go_live_button_if_checklist_not_complete( mock_get_service_templates, mock_get_users_by_service, mock_get_service_organisation, + mock_get_invites_for_service, single_sms_sender, checklist_completed, agreement_signed, @@ -919,6 +932,7 @@ def test_should_check_for_sms_sender_on_go_live( service_one, mocker, mock_get_service_organisation, + mock_get_invites_for_service, organisation_type, count_of_sms_templates, sms_senders, @@ -997,6 +1011,7 @@ def test_should_check_for_mou_on_request_to_go_live( service_one, mocker, agreement_signed, + mock_get_invites_for_service, expected_item, ): mocker.patch( @@ -1041,6 +1056,7 @@ def test_should_check_for_mou_on_request_to_go_live( def test_non_gov_user_is_told_they_cant_go_live( client_request, api_nongov_user_active, + mock_get_invites_for_service, mocker, mock_get_service_organisation, ): @@ -1319,6 +1335,7 @@ def test_should_redirect_after_request_to_go_live( mock_get_service_templates, mock_get_users_by_service, mock_update_service, + mock_get_invites_without_manage_permission, volumes, displayed_volumes, formatted_displayed_volumes, @@ -1608,6 +1625,7 @@ def test_route_permissions( single_reply_to_email_address, single_letter_contact_block, mock_get_service_organisation, + mock_get_invites_for_service, single_sms_sender, route, mock_get_service_settings_page_common, @@ -1641,6 +1659,7 @@ def test_route_invalid_permissions( service_one, route, mock_get_service_templates, + mock_get_invites_for_service, ): validate_route_permission( mocker, @@ -1673,6 +1692,7 @@ def test_route_for_platform_admin( route, mock_get_service_settings_page_common, mock_get_service_templates, + mock_get_invites_for_service, ): validate_route_permission(mocker, app_, diff --git a/tests/conftest.py b/tests/conftest.py index 6fa21eae0..ca6f95287 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2258,6 +2258,25 @@ def mock_get_invites_for_service(mocker, service_one, sample_invite): return mocker.patch('app.invite_api_client._get_invites_for_service', side_effect=_get_invites) +@pytest.fixture(scope='function') +def mock_get_invites_without_manage_permission(mocker, service_one, sample_invite): + + def _get_invites(service_id): + return [invite_json( + id_=str(sample_uuid()), + from_user=service_one['users'][0], + email_address='invited_user@test.gov.uk', + service_id=service_one['id'], + permissions='view_activity,send_messages,manage_api_keys', + created_at=str(datetime.utcnow()), + auth_type='sms_auth', + folder_permissions=[], + status='pending', + )] + + return mocker.patch('app.invite_api_client._get_invites_for_service', side_effect=_get_invites) + + @pytest.fixture(scope='function') def mock_check_invite_token(mocker, sample_invite): def _check_token(token):