From e0b61e3d704aadf776f8c8e58511f47934ae5ae3 Mon Sep 17 00:00:00 2001 From: Pea Tyczynska Date: Thu, 4 Apr 2019 12:05:17 +0100 Subject: [PATCH 01/31] Call precompiled letter preview with overlay --- app/main/views/notifications.py | 27 +++++++++++++------- app/notify_client/notification_api_client.py | 11 ++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/main/views/notifications.py b/app/main/views/notifications.py index 3e031c632..00765ebbb 100644 --- a/app/main/views/notifications.py +++ b/app/main/views/notifications.py @@ -60,7 +60,9 @@ def view_notification(service_id, notification_id): if notification['template']['is_precompiled_letter']: try: - file_contents = view_letter_notification_as_preview(service_id, notification_id, "pdf") + file_contents = view_letter_notification_as_preview( + service_id, notification_id, "pdf" + ) page_count = pdf_page_count(io.BytesIO(file_contents)) except PdfReadError: return render_template( @@ -72,7 +74,6 @@ def view_notification(service_id, notification_id): if notification.get('postage'): notification['template']['postage'] = notification['postage'] - template = get_template( notification['template'], current_service, @@ -173,14 +174,22 @@ def view_letter_notification_as_preview(service_id, notification_id, filetype): if filetype not in ('pdf', 'png'): abort(404) - + notification = notification_api_client.get_notification(service_id, str(notification_id)) try: - preview = notification_api_client.get_notification_letter_preview( - service_id, - notification_id, - filetype, - page=request.args.get('page') - ) + if notification['status'] == "validation-failed": + preview = notification_api_client.get_notification_letter_preview_with_overlay( + service_id, + notification_id, + filetype, + page=request.args.get('page') + ) + else: + preview = notification_api_client.get_notification_letter_preview( + service_id, + notification_id, + filetype, + page=request.args.get('page') + ) display_file = base64.b64decode(preview['content']) except APIError: diff --git a/app/notify_client/notification_api_client.py b/app/notify_client/notification_api_client.py index 579c93860..50d483ee9 100644 --- a/app/notify_client/notification_api_client.py +++ b/app/notify_client/notification_api_client.py @@ -94,6 +94,17 @@ class NotificationApiClient(NotifyAdminAPIClient): return self.get(url=get_url) + def get_notification_letter_preview_with_overlay(self, service_id, notification_id, file_type, page=None): + get_url = '/service/{}/template/preview/{}/{}{}{}'.format( + service_id, + notification_id, + file_type, + '?overlay=1', + '&page={}'.format(page) if page else '', + ) + + return self.get(url=get_url) + def update_notification_to_cancelled(self, service_id, notification_id): return self.post( url='/service/{}/notifications/{}/cancel'.format(service_id, notification_id), From 858e98736f9908b4817796076c40032f83117a8c Mon Sep 17 00:00:00 2001 From: Pea Tyczynska Date: Thu, 4 Apr 2019 13:41:30 +0100 Subject: [PATCH 02/31] Test that preview with overlay gets called --- tests/app/main/views/test_notifications.py | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/app/main/views/test_notifications.py b/tests/app/main/views/test_notifications.py index c003623d0..2a99eb110 100644 --- a/tests/app/main/views/test_notifications.py +++ b/tests/app/main/views/test_notifications.py @@ -456,6 +456,32 @@ def test_should_show_image_of_letter_notification( assert response.get_data(as_text=True) == 'foo' +def test_should_show_image_of_letter_notification_that_failed_validation( + logged_in_client, + fake_uuid, + mocker +): + + mock_get_notification(mocker, fake_uuid, template_type='letter', notification_status='validation-failed') + + mocker.patch( + 'app.main.views.notifications.notification_api_client.get_notification_letter_preview_with_overlay', + return_value={ + 'content': base64.b64encode(b'foo').decode('utf-8') + } + ) + + response = logged_in_client.get(url_for( + 'main.view_letter_notification_as_preview', + service_id=SERVICE_ONE_ID, + notification_id=fake_uuid, + filetype='png' + )) + + assert response.status_code == 200 + assert response.get_data(as_text=True) == 'foo' + + def test_should_show_preview_error_image_letter_notification_on_preview_error( logged_in_client, fake_uuid, From 3c302e459512224ab816f8df762a5b6be1331a25 Mon Sep 17 00:00:00 2001 From: Alexey Bezhan Date: Wed, 10 Apr 2019 11:04:55 +0100 Subject: [PATCH 03/31] Stop pyup from trying to upgrade gunicorn --- requirements-app.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-app.txt b/requirements-app.txt index 3dadce190..a4ca59d71 100644 --- a/requirements-app.txt +++ b/requirements-app.txt @@ -13,7 +13,7 @@ pyexcel-xls==0.5.8 pyexcel-xlsx==0.5.7 pyexcel-ods3==0.5.3 pytz==2018.9 -gunicorn==19.7.1 # >19.8 stops eventlet workers after a timeout +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 ab2ecc76b..e2bb6204d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ pyexcel-xls==0.5.8 pyexcel-xlsx==0.5.7 pyexcel-ods3==0.5.3 pytz==2018.9 -gunicorn==19.7.1 # >19.8 stops eventlet workers after a timeout +gunicorn==19.7.1 # pyup: ignore, >19.8 breaks eventlet patching eventlet==0.24.1 notifications-python-client==5.3.0 From 436d023722569acba79e43da8971193b80e7a6ea Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 11 Apr 2019 14:35:30 +0100 Subject: [PATCH 04/31] Block incomplete requests to go live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dealing with users who request to go live but haven’t completed all the steps still represents a significant support overhead for our team. We’ve made some improvements to the percentage of incomplete requests with a better page design, but ultimately because it still shows the button people think it’s OK to press the button while some of the items on the page still say [Not completed]. We can do this now because organisations are in the database, which means we can mark the agreement signed as soon as we get it back, without having to deploy code. --- .../service-settings/request-to-go-live.html | 14 ++-- tests/app/main/views/test_service_settings.py | 77 ++++++++++++++++--- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/app/templates/views/service-settings/request-to-go-live.html b/app/templates/views/service-settings/request-to-go-live.html index 1483e79bc..d1471268e 100644 --- a/app/templates/views/service-settings/request-to-go-live.html +++ b/app/templates/views/service-settings/request-to-go-live.html @@ -52,7 +52,15 @@ ) }} {% endif %} {% endcall %} - {% if current_user.is_gov_user %} + {% if not current_user.is_gov_user %} +

+ Only team members with a government email address can request to go live. +

+ {% elif (not current_service.go_live_checklist_completed) or (show_agreement and not agreement_signed) %} +

+ No go +

+ {% else %}

When we receive your request we’ll get back to you within one working day.

@@ -62,10 +70,6 @@ {% call form_wrapper() %} {{ page_footer('Request to go live') }} {% endcall %} - {% else %} -

- Only team members with a government email address can request to go live. -

{% endif %} diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index dc81457af..3b638ac89 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -1,3 +1,4 @@ +from collections import namedtuple from functools import partial from unittest.mock import ANY, PropertyMock, call from urllib.parse import parse_qs, urlparse @@ -731,18 +732,78 @@ def test_should_check_for_sending_things_right( assert normalize_spaces(checklist_items[2].text) == expected_templates_checklist_item assert normalize_spaces(checklist_items[3].text) == expected_reply_to_checklist_item - assert page.select_one('form')['method'] == 'post' - assert 'action' not in page.select_one('form') - mock_count_users.assert_called_once_with(SERVICE_ONE_ID, 'manage_service') - assert mock_templates.call_args_list == [ - call(), - ] + assert mock_templates.called is True if count_of_email_templates: mock_get_reply_to_email_addresses.assert_called_once_with(SERVICE_ONE_ID) +@pytest.mark.parametrize('checklist_completed, agreement_signed, expected_button', ( + (True, True, True), + (True, None, True), + (True, False, False), + (False, True, False), + (False, None, False), +)) +def test_should_not_show_go_live_button_if_checklist_not_complete( + client_request, + mocker, + mock_get_service_templates, + mock_get_users_by_service, + single_sms_sender, + checklist_completed, + agreement_signed, + expected_button, +): + + def _agreement_info(): + return namedtuple( + 'AgreementInfo', ['agreement_signed'] + )(agreement_signed=agreement_signed) + + mocker.patch( + 'app.models.service.Service.go_live_checklist_completed', + new_callable=PropertyMock, + return_value=checklist_completed, + ) + mocker.patch( + 'app.utils.AgreementInfo.from_current_user', + side_effect=_agreement_info, + ) + + for channel in ('email', 'sms', 'letter'): + mocker.patch( + 'app.models.service.Service.volume_{}'.format(channel), + create=True, + new_callable=PropertyMock, + return_value=0, + ) + + page = client_request.get( + 'main.request_to_go_live', service_id=SERVICE_ONE_ID + ) + assert page.h1.text == 'Before you request to go live' + + if expected_button: + assert page.select_one('form')['method'] == 'post' + assert 'action' not in page.select_one('form') + assert normalize_spaces(page.select('main p')[0].text) == ( + 'When we receive your request we’ll get back to you within one working day.' + ) + assert normalize_spaces(page.select('main p')[1].text) == ( + 'By requesting to go live you’re agreeing to our terms of use.' + ) + page.select_one('[type=submit]').text.strip() == ('Request to go live') + else: + assert not page.select('form') + assert not page.select('[type=submit]') + assert len(page.select('main p')) == 1 + assert normalize_spaces(page.select_one('main p').text) == ( + 'No go' + ) + + @pytest.mark.parametrize(( 'estimated_sms_volume,' 'organisation_type,' @@ -887,9 +948,7 @@ def test_should_check_for_sms_sender_on_go_live( checklist_items = page.select('.task-list .task-list-item') assert normalize_spaces(checklist_items[3].text) == expected_sms_sender_checklist_item - assert mock_templates.call_args_list == [ - call(), - ] + assert mock_templates.called is True mock_get_sms_senders.assert_called_once_with(SERVICE_ONE_ID) From f997b446ff192759d7b0a67ae9e6bc89fa003a2e Mon Sep 17 00:00:00 2001 From: karlchillmaid Date: Thu, 11 Apr 2019 15:43:26 +0100 Subject: [PATCH 05/31] Update app/templates/views/service-settings/request-to-go-live.html Co-Authored-By: quis --- app/templates/views/service-settings/request-to-go-live.html | 2 +- tests/app/main/views/test_service_settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/views/service-settings/request-to-go-live.html b/app/templates/views/service-settings/request-to-go-live.html index d1471268e..45d66f00e 100644 --- a/app/templates/views/service-settings/request-to-go-live.html +++ b/app/templates/views/service-settings/request-to-go-live.html @@ -58,7 +58,7 @@

{% elif (not current_service.go_live_checklist_completed) or (show_agreement and not agreement_signed) %}

- No go + You must complete these steps before you can request to go live.

{% else %}

diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 3b638ac89..847bc3536 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -800,7 +800,7 @@ def test_should_not_show_go_live_button_if_checklist_not_complete( assert not page.select('[type=submit]') assert len(page.select('main p')) == 1 assert normalize_spaces(page.select_one('main p').text) == ( - 'No go' + 'You must complete these steps before you can request to go live.' ) From 9863aa3c48ce81b9d77e44c96c9ea4bca78d5a81 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Wed, 10 Apr 2019 17:20:51 +0100 Subject: [PATCH 06/31] Automate counting of live services and orgs Returns the data calculated by the API. Stored in Redis against a hardcoded key so that no-one hammering the home page is directly hitting the database. --- app/main/views/index.py | 9 +++++++-- app/main/views/service_settings.py | 7 +------ app/models/service.py | 3 +++ app/notify_client/organisations_api_client.py | 1 + app/notify_client/service_api_client.py | 8 ++++++++ app/notify_client/status_api_client.py | 7 +++++-- app/templates/views/signedout.html | 6 +++--- tests/app/main/views/test_headers.py | 14 ++++++++++++-- tests/app/main/views/test_index.py | 13 +++++++++++++ tests/conftest.py | 8 ++++++++ 10 files changed, 61 insertions(+), 15 deletions(-) diff --git a/app/main/views/index.py b/app/main/views/index.py index 1247c0aa2..2d7423661 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -12,7 +12,7 @@ from notifications_utils.international_billing_rates import ( ) from notifications_utils.template import HTMLEmailTemplate, LetterImageTemplate -from app import email_branding_client, letter_branding_client +from app import email_branding_client, letter_branding_client, status_api_client from app.main import main from app.main.forms import FieldWithNoneOption, SearchByNameForm from app.main.views.sub_navigation_dictionaries import features_nav @@ -21,9 +21,14 @@ from app.utils import AgreementInfo, get_logo_cdn_domain @main.route('/') def index(): + if current_user and current_user.is_authenticated: return redirect(url_for('main.choose_account')) - return render_template('views/signedout.html') + + return render_template( + 'views/signedout.html', + counts=status_api_client.get_count_of_live_services_and_organisations(), + ) @main.route('/robots.txt') diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index ba20ccbb1..f7989fe14 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -262,12 +262,7 @@ def service_switch_live(service_id): ) if form.validate_on_submit(): - current_service.update( - # TODO This limit should be set depending on the agreement signed by - # with Notify. - message_limit=250000 if form.enabled.data else 50, - restricted=(not form.enabled.data) - ) + current_service.update_status(live=form.enabled.data) return redirect(url_for('.service_settings', service_id=service_id)) return render_template( diff --git a/app/models/service.py b/app/models/service.py index 64805f430..30ee5fc0c 100644 --- a/app/models/service.py +++ b/app/models/service.py @@ -72,6 +72,9 @@ class Service(): def update(self, **kwargs): return service_api_client.update_service(self.id, **kwargs) + def update_status(self, live): + return service_api_client.update_status(self.id, live=live) + def switch_permission(self, permission): return self.force_permission( permission, diff --git a/app/notify_client/organisations_api_client.py b/app/notify_client/organisations_api_client.py index d1d082e82..fab82246e 100644 --- a/app/notify_client/organisations_api_client.py +++ b/app/notify_client/organisations_api_client.py @@ -25,6 +25,7 @@ class OrganisationsClient(NotifyAdminAPIClient): return self.get(url="/service/{}/organisation".format(service_id)) @cache.delete('service-{service_id}') + @cache.delete('live-service-and-organisation-counts') def update_service_organisation(self, service_id, org_id): data = { 'service_id': service_id diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index b2242ae1a..b79651dbd 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -98,6 +98,14 @@ class ServiceAPIClient(NotifyAdminAPIClient): endpoint = "/service/{0}".format(service_id) return self.post(endpoint, data) + @cache.delete('live-service-and-organisation-counts') + def update_status(self, service_id, live): + return self.update_service( + service_id, + message_limit=250000 if live else 50, + restricted=(not live), + ) + # This method is not cached because it calls through to one which is def update_service_with_properties(self, service_id, properties): return self.update_service(service_id, **properties) diff --git a/app/notify_client/status_api_client.py b/app/notify_client/status_api_client.py index 3925a77bd..7228621c6 100644 --- a/app/notify_client/status_api_client.py +++ b/app/notify_client/status_api_client.py @@ -1,5 +1,4 @@ - -from app.notify_client import NotifyAdminAPIClient +from app.notify_client import NotifyAdminAPIClient, cache class StatusApiClient(NotifyAdminAPIClient): @@ -7,5 +6,9 @@ class StatusApiClient(NotifyAdminAPIClient): def get_status(self, *params): return self.get(url='/_status', *params) + @cache.set('live-service-and-organisation-counts') + def get_count_of_live_services_and_organisations(self): + return self.get(url='/_status/live-service-and-organisation-counts') + status_api_client = StatusApiClient() diff --git a/app/templates/views/signedout.html b/app/templates/views/signedout.html index ae7ab262c..14b5d545e 100644 --- a/app/templates/views/signedout.html +++ b/app/templates/views/signedout.html @@ -115,17 +115,17 @@

-
+

Who’s using GOV.UK Notify

Services

-
745
+
{{ counts.services }}
services

Organisations

-
233
+
{{ counts.organisations }}
organisations
diff --git a/tests/app/main/views/test_headers.py b/tests/app/main/views/test_headers.py index 853bbba1a..9f5858240 100644 --- a/tests/app/main/views/test_headers.py +++ b/tests/app/main/views/test_headers.py @@ -1,4 +1,10 @@ -def test_owasp_useful_headers_set(client, mocker): + + +def test_owasp_useful_headers_set( + client, + mocker, + mock_get_service_and_organisation_counts, +): mocker.patch('app.get_logo_cdn_domain', return_value='static-logos.test.com') response = client.get('/') @@ -19,7 +25,11 @@ def test_owasp_useful_headers_set(client, mocker): ) -def test_headers_non_ascii_characters_are_replaced(client, mocker): +def test_headers_non_ascii_characters_are_replaced( + client, + mocker, + mock_get_service_and_organisation_counts, +): mocker.patch('app.get_logo_cdn_domain', return_value='static-logos€æ.test.com') response = client.get('/') diff --git a/tests/app/main/views/test_index.py b/tests/app/main/views/test_index.py index 211d2b25e..2d19801e0 100644 --- a/tests/app/main/views/test_index.py +++ b/tests/app/main/views/test_index.py @@ -14,6 +14,7 @@ from tests.conftest import ( def test_non_logged_in_user_can_see_homepage( client, + mock_get_service_and_organisation_counts, ): response = client.get(url_for('main.index')) assert response.status_code == 200 @@ -30,6 +31,18 @@ def test_non_logged_in_user_can_see_homepage( 'local authority, or the NHS.' ) + assert normalize_spaces(page.select_one('#whos-using-notify').text) == ( + 'Who’s using GOV.UK Notify ' + 'Services ' + '9999 services ' + 'Organisations ' + '111 organisations ' + 'See the list of services and organisations.' + ) + assert page.select_one('#whos-using-notify a')['href'] == ( + 'https://www.gov.uk/performance/govuk-notify/government-services' + ) + def test_logged_in_user_redirects_to_choose_account( client_request, diff --git a/tests/conftest.py b/tests/conftest.py index b07b375da..ddc2c3efa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3351,3 +3351,11 @@ def mock_move_to_template_folder(mocker): @pytest.fixture def mock_create_template_folder(mocker): return mocker.patch('app.template_folder_api_client.create_template_folder', return_value=sample_uuid()) + + +@pytest.fixture(scope='function') +def mock_get_service_and_organisation_counts(mocker): + return mocker.patch('app.status_api_client.get_count_of_live_services_and_organisations', return_value={ + 'organisations': 111, + 'services': 9999, + }) From 718f4407203ccbb253dd3879536d7d5a3781132f Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 4 Apr 2019 11:18:22 +0100 Subject: [PATCH 07/31] Get info about organisations from database table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the first step of replacing the `domains.yml` file. In order to replicate the same functionality we get from the `domains.yml` file and its associated code this commit adds a `Organisation` model. This model copies a lot of methods from the `AgreementInfo` class which wrapped the `domains.yml` file. It factors out some stuff that would otherwise be duplicated between the `Organisation` and `Service` model, in such a way that could be reused for making other models in the future. This commit doesn’t change other parts of the code to make use of this new model yet – that will come in subsequent commits. --- app/models/__init__.py | 27 ++++ app/models/organisation.py | 140 ++++++++++++++++++ app/models/service.py | 31 ++-- app/models/user.py | 17 +++ app/notify_client/organisations_api_client.py | 12 ++ 5 files changed, 209 insertions(+), 18 deletions(-) create mode 100644 app/models/__init__.py create mode 100644 app/models/organisation.py diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 000000000..f9efbffeb --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,27 @@ +from flask import abort + + +class JSONModel(): + + ALLOWED_PROPERTIES = set() + + def __init__(self, _dict): + # in the case of a bad request _dict may be `None` + self._dict = _dict or {} + + def __bool__(self): + return self._dict != {} + + def __getattr__(self, attr): + if attr in self.ALLOWED_PROPERTIES: + return self._dict[attr] + raise AttributeError('`{}` is not a {} attribute'.format( + attr, + self.__class__.__name__.lower(), + )) + + def _get_by_id(self, things, id): + try: + return next(thing for thing in things if thing['id'] == str(id)) + except StopIteration: + abort(404) diff --git a/app/models/organisation.py b/app/models/organisation.py new file mode 100644 index 000000000..d3753a431 --- /dev/null +++ b/app/models/organisation.py @@ -0,0 +1,140 @@ +from flask import Markup, abort + +from app.models import JSONModel + + +class Organisation(JSONModel): + + ALLOWED_PROPERTIES = { + 'id', + 'name', + 'active', + 'crown', + 'organisation_type', + 'letter_branding_id', + 'email_branding_id', + 'agreement_signed', + 'agreement_signed_at', + 'agreement_signed_by_id', + 'agreement_signed_version', + 'domains', + } + + def __init__(self, _dict): + + super().__init__(_dict) + + if self._dict == {}: + self.name, self.crown, self.agreement_signed = None, None, None + + @property + def crown_status(self): + return self.crown + + @property + def as_human_readable(self): + if 'dwp.' in ''.join(self.domains): + return 'DWP - Requires OED approval' + if self.agreement_signed: + return 'Yes, on behalf of {}'.format(self.name) + elif self.owner: + return '{} (organisation is {}, {})'.format( + { + False: 'No', + None: 'Can’t tell', + }.get(self.agreement_signed), + self.name, + { + True: 'a crown body', + False: 'a non-crown body', + None: 'crown status unknown', + }.get(self.crown_status), + ) + else: + return 'Can’t tell (domain is {})'.format(self._domain) + + @property + def as_info_for_branding_request(self): + return self.owner or 'Can’t tell (domain is {})'.format(self._domain) + + @property + def as_jinja_template(self): + if self.crown_status is None: + return 'agreement-choose' + if self.agreement_signed: + return 'agreement-signed' + return 'agreement' + + def as_terms_of_use_paragraph(self, **kwargs): + return Markup(self._as_terms_of_use_paragraph(**kwargs)) + + def _as_terms_of_use_paragraph(self, terms_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.' + ).format(self._acceptance_required, terms_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(self._acceptance_required, download_link, support_link)) + + if self.agreement_signed is False: + return (( + '{} Download a copy.' + ).format(self._acceptance_required, download_link)) + + return ( + 'Your organisation ({}) has already accepted the ' + 'GOV.UK Notify data sharing and financial ' + '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 ( + 'Your organisation {} must also accept our data sharing ' + 'and financial agreement.'.format( + '({})'.format(self.name) if self.name else '', + ) + ) + + @property + def crown_status_or_404(self): + if self.crown_status is None: + abort(404) + return self.crown_status diff --git a/app/models/service.py b/app/models/service.py index 64805f430..425fceb02 100644 --- a/app/models/service.py +++ b/app/models/service.py @@ -2,6 +2,8 @@ from flask import abort, current_app from notifications_utils.field import Field from werkzeug.utils import cached_property +from app.models import JSONModel +from app.models.organisation import Organisation from app.notify_client.api_key_api_client import api_key_api_client from app.notify_client.billing_api_client import billing_api_client from app.notify_client.email_branding_client import email_branding_client @@ -18,7 +20,7 @@ from app.notify_client.user_api_client import user_api_client from app.utils import get_default_sms_sender -class Service(): +class Service(JSONModel): ALLOWED_PROPERTIES = { 'active', @@ -50,25 +52,12 @@ class Service(): ) def __init__(self, _dict): - # in the case of a bad request current service may be `None` - self._dict = _dict or {} + + super().__init__(_dict) + if 'permissions' not in self._dict: self.permissions = {'email', 'sms', 'letter'} - def __bool__(self): - return self._dict != {} - - def __getattr__(self, attr): - if attr in self.ALLOWED_PROPERTIES: - return self._dict[attr] - raise AttributeError('`{}` is not a service attribute'.format(attr)) - - def _get_by_id(self, things, id): - try: - return next(thing for thing in things if thing['id'] == str(id)) - except StopIteration: - abort(404) - def update(self, **kwargs): return service_api_client.update_service(self.id, **kwargs) @@ -402,8 +391,14 @@ class Service(): return None @cached_property + def organisation(self): + return Organisation( + organisations_client.get_service_organisation(self.id) + ) + + @property def organisation_name(self): - return organisations_client.get_service_organisation(self.id).get('name', None) + return self.organisation.name @cached_property def inbound_number(self): diff --git a/app/models/user.py b/app/models/user.py index e54315ed5..e9ec02707 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -2,7 +2,10 @@ from itertools import chain from flask import abort, request, session from flask_login import AnonymousUserMixin, UserMixin +from werkzeug.utils import cached_property +from app.models.organisation import Organisation +from app.notify_client.organisations_api_client import organisations_client from app.utils import is_gov_user roles = { @@ -192,6 +195,16 @@ class User(UserMixin): def is_locked(self): return self.failed_login_count >= self.max_failed_login_count + @property + def email_domain(self): + return self.email_address.split('@')[-1] + + @cached_property + def default_organisation(self): + return Organisation( + organisations_client.get_organisation_by_domain(self.email_domain) + ) + def serialize(self): dct = { "id": self.id, @@ -322,3 +335,7 @@ class AnonymousUser(AnonymousUserMixin): # set the anonymous user so that if a new browser hits us we don't error http://stackoverflow.com/a/19275188 def logged_in_elsewhere(self): return False + + @property + def default_organisation(self): + return Organisation(None) diff --git a/app/notify_client/organisations_api_client.py b/app/notify_client/organisations_api_client.py index d1d082e82..ed54537cc 100644 --- a/app/notify_client/organisations_api_client.py +++ b/app/notify_client/organisations_api_client.py @@ -1,3 +1,5 @@ +from notifications_python_client.errors import HTTPError + from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache @@ -9,6 +11,16 @@ class OrganisationsClient(NotifyAdminAPIClient): def get_organisation(self, org_id): return self.get(url='/organisations/{}'.format(org_id)) + def get_organisation_by_domain(self, domain): + try: + return self.get( + url='/organisations/by-domain?domain={}'.format(domain), + ) + except HTTPError as error: + if error.status_code == 404: + return None + raise error + def create_organisation(self, name): data = { "name": name From 72f49a9a1e93c313d8fa27eefb8c532726dc9ede Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 4 Apr 2019 11:12:53 +0100 Subject: [PATCH 08/31] Remove dependence on domains.yml from static pages This will allow us to remove the `domains.yml` file, by using information about organisations that is now stored in the database instead. --- app/main/views/index.py | 4 +--- app/templates/views/pricing.html | 2 +- app/templates/views/terms-of-use.html | 2 +- tests/__init__.py | 10 +++++---- tests/app/main/views/test_index.py | 31 +++++++++++++++++---------- tests/conftest.py | 21 ++++++++++++++++++ 6 files changed, 50 insertions(+), 20 deletions(-) diff --git a/app/main/views/index.py b/app/main/views/index.py index 1247c0aa2..f53bcdbb2 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -16,7 +16,7 @@ from app import email_branding_client, letter_branding_client from app.main import main from app.main.forms import FieldWithNoneOption, SearchByNameForm from app.main.views.sub_navigation_dictionaries import features_nav -from app.utils import AgreementInfo, get_logo_cdn_domain +from app.utils import get_logo_cdn_domain @main.route('/') @@ -75,7 +75,6 @@ def pricing(): for cc, country in INTERNATIONAL_BILLING_RATES.items() ], key=lambda x: x[0]), search_form=SearchByNameForm(), - agreement_info=AgreementInfo.from_current_user(), ) @@ -249,7 +248,6 @@ def terms(): return render_template( 'views/terms-of-use.html', navigation_links=features_nav(), - agreement_info=AgreementInfo.from_current_user(), ) diff --git a/app/templates/views/pricing.html b/app/templates/views/pricing.html index cad28eb2e..3a4538716 100644 --- a/app/templates/views/pricing.html +++ b/app/templates/views/pricing.html @@ -123,7 +123,7 @@

How to pay

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

- {{ agreement_info.as_pricing_paragraph( + {{ 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'), diff --git a/app/templates/views/terms-of-use.html b/app/templates/views/terms-of-use.html index f0576bbbd..7962422c5 100644 --- a/app/templates/views/terms-of-use.html +++ b/app/templates/views/terms-of-use.html @@ -21,7 +21,7 @@ Terms of use

- {{ agreement_info.as_terms_of_use_paragraph( + {{ current_user.default_organisation.as_terms_of_use_paragraph( terms_link=url_for('main.sign_in', next=url_for('main.terms')), download_link=url_for('.agreement'), support_link=url_for('.feedback', ticket_type='ask-question-give-feedback', body='agreement'), diff --git a/tests/__init__.py b/tests/__init__.py index c3c934454..cbe1405b3 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -184,7 +184,7 @@ def service_json( def organisation_json( id_='1234', - name='Test Organisation', + name=False, users=None, active=True, created_at=None, @@ -192,6 +192,8 @@ def organisation_json( letter_branding_id=None, email_branding_id=None, domains=None, + crown=True, + agreement_signed=False, ): if users is None: users = [] @@ -199,7 +201,7 @@ def organisation_json( services = [] return { 'id': id_, - 'name': name, + 'name': 'Test Organisation' if name is False else name, 'active': active, 'users': users, 'services': services, @@ -207,8 +209,8 @@ def organisation_json( 'email_branding_id': email_branding_id, 'letter_branding_id': letter_branding_id, 'organisation_type': '', - 'crown': True, - 'agreement_signed': False, + 'crown': crown, + 'agreement_signed': agreement_signed, 'agreement_signed_at': None, 'agreement_signed_by': None, 'domains': domains or [], diff --git a/tests/app/main/views/test_index.py b/tests/app/main/views/test_index.py index 211d2b25e..ec0896d29 100644 --- a/tests/app/main/views/test_index.py +++ b/tests/app/main/views/test_index.py @@ -6,7 +6,7 @@ from flask import url_for from app.main.forms import FieldWithNoneOption from tests.conftest import ( - active_user_with_permissions, + mock_get_organisation_by_domain, normalize_spaces, sample_uuid, ) @@ -69,6 +69,7 @@ def test_robots(client): ]) def test_static_pages( client_request, + mock_get_organisation_by_domain, view, ): page = client_request.get('main.{}'.format(view)) @@ -166,13 +167,15 @@ def test_pricing_is_generic_if_user_is_not_logged_in( @pytest.mark.parametrize(( - 'email_address,' + 'name,' + 'agreement_signed,' 'expected_terms_paragraph,' 'expected_terms_link,' 'expected_pricing_paragraph' ), [ ( - 'test@cabinet-office.gov.uk', + 'Cabinet Office', + True, ( 'Your organisation (Cabinet Office) has already accepted ' 'the GOV.UK Notify data sharing and financial agreement.' @@ -184,7 +187,8 @@ def test_pricing_is_generic_if_user_is_not_logged_in( ), ), ( - 'test@aylesburytowncouncil.gov.uk', + 'Aylesbury Town Council', + False, ( 'Your organisation (Aylesbury Town Council) must also ' 'accept our data sharing and financial agreement. Download ' @@ -200,7 +204,8 @@ def test_pricing_is_generic_if_user_is_not_logged_in( ), ), ( - 'larry@downing-street.gov.uk', + None, + None, ( 'Your organisation must also accept our data sharing and ' 'financial agreement. Download the agreement or contact us ' @@ -217,7 +222,8 @@ def test_pricing_is_generic_if_user_is_not_logged_in( ), ), ( - 'michael.fish@metoffice.gov.uk', + 'Met Office', + False, ( 'Your organisation (Met Office) must also accept our data ' 'sharing and financial agreement. Download a copy.' @@ -236,14 +242,17 @@ def test_terms_tells_logged_in_users_what_we_know_about_their_agreement( mocker, fake_uuid, client_request, - email_address, + name, + agreement_signed, expected_terms_paragraph, expected_terms_link, expected_pricing_paragraph, ): - user = active_user_with_permissions(fake_uuid) - user.email_address = email_address - mocker.patch('app.user_api_client.get_user', return_value=user) + mock_get_organisation_by_domain( + mocker, + name=name, + 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 @@ -256,7 +265,7 @@ def test_terms_tells_logged_in_users_what_we_know_about_their_agreement( def test_css_is_served_from_correct_path(client_request): - page = client_request.get('main.pricing') # easy static page + page = client_request.get('main.documentation') # easy static page for index, link in enumerate( page.select('link[rel=stylesheet]') diff --git a/tests/conftest.py b/tests/conftest.py index b07b375da..0ba53ab17 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3125,6 +3125,27 @@ def mock_get_organisation( return mocker.patch('app.organisations_client.get_organisation', side_effect=_get_organisation) +@pytest.fixture(scope='function') +def mock_get_organisation_by_domain( + mocker, + name=False, + crown=True, + agreement_signed=False, +): + def _get_organisation_by_domain(org_id): + return organisation_json( + org_id, + name, + crown=crown, + agreement_signed=agreement_signed, + ) + + return mocker.patch( + 'app.organisations_client.get_organisation_by_domain', + side_effect=_get_organisation_by_domain, + ) + + @pytest.fixture(scope='function') def mock_get_service_organisation(mocker): def _get_service_organisation(service_id): From 98249158cb687b7c7e732a055971894545ffe483 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 4 Apr 2019 11:15:42 +0100 Subject: [PATCH 09/31] Stop trying to infer branding when adding services The API handles this now. --- app/main/views/add_service.py | 10 +---- tests/app/main/views/test_add_service.py | 55 ++---------------------- tests/conftest.py | 2 - 3 files changed, 6 insertions(+), 61 deletions(-) diff --git a/app/main/views/add_service.py b/app/main/views/add_service.py index 8e6d942f1..8fce160ae 100644 --- a/app/main/views/add_service.py +++ b/app/main/views/add_service.py @@ -2,17 +2,15 @@ from flask import current_app, redirect, render_template, session, url_for from flask_login import login_required from notifications_python_client.errors import HTTPError -from app import billing_api_client, email_branding_client, service_api_client +from app import billing_api_client, service_api_client from app.main import main from app.main.forms import CreateServiceForm -from app.utils import AgreementInfo, email_safe, user_is_gov_user +from app.utils import email_safe, user_is_gov_user def _create_service(service_name, organisation_type, email_from, form): free_sms_fragment_limit = current_app.config['DEFAULT_FREE_SMS_FRAGMENT_LIMITS'].get(organisation_type) - domain = 'nhs.uk' if organisation_type == 'nhs' else AgreementInfo.from_current_user().canonical_domain - email_branding = email_branding_client.get_email_branding_id_for_domain(domain) try: service_id = service_api_client.create_service( service_name=service_name, @@ -21,13 +19,9 @@ def _create_service(service_name, organisation_type, email_from, form): restricted=True, user_id=session['user_id'], email_from=email_from, - service_domain=domain ) session['service_id'] = service_id - if email_branding: - service_api_client.update_service(service_id, email_branding=email_branding) - billing_api_client.create_or_update_free_sms_fragment_limit(service_id, free_sms_fragment_limit) return service_id, None diff --git a/tests/app/main/views/test_add_service.py b/tests/app/main/views/test_add_service.py index 5395366e7..18d00b199 100644 --- a/tests/app/main/views/test_add_service.py +++ b/tests/app/main/views/test_add_service.py @@ -56,7 +56,6 @@ def test_should_add_service_and_redirect_to_tour_when_no_services( restricted=True, user_id=api_user_active.id, email_from='testing.the.post', - service_domain=None ) mock_create_service_template.assert_called_once_with( 'Example text message template', @@ -71,10 +70,10 @@ def test_should_add_service_and_redirect_to_tour_when_no_services( mock_create_or_update_free_sms_fragment_limit.assert_called_once_with(101, 25000) -@pytest.mark.parametrize('organisation_type, free_allowance, service_domain', [ - ('central', 250 * 1000, None), - ('local', 25 * 1000, None), - ('nhs', 25 * 1000, 'nhs.uk'), +@pytest.mark.parametrize('organisation_type, free_allowance', [ + ('central', 250 * 1000), + ('local', 25 * 1000), + ('nhs', 25 * 1000), ]) def test_should_add_service_and_redirect_to_dashboard_when_existing_service( app_, @@ -82,11 +81,9 @@ def test_should_add_service_and_redirect_to_dashboard_when_existing_service( mock_create_service, mock_create_service_template, mock_get_services, - mock_update_service, api_user_active, organisation_type, free_allowance, - service_domain, mock_create_or_update_free_sms_fragment_limit, mock_get_all_email_branding, ): @@ -111,56 +108,12 @@ def test_should_add_service_and_redirect_to_dashboard_when_existing_service( restricted=True, user_id=api_user_active.id, email_from='testing.the.post', - service_domain=service_domain ) mock_create_or_update_free_sms_fragment_limit.assert_called_once_with(101, free_allowance) assert len(mock_create_service_template.call_args_list) == 0 assert session['service_id'] == 101 -@pytest.mark.parametrize('organisation_type, email_address, expected_branding', [ - ('central', 'test@example.voa.gsi.gov.uk', '5'), - ('central', 'test@example.voa.gov.uk', '5'), - ('central', 'test@example.gov.uk', None), - # Anyone choosing ‘NHS’ for organisation type gets NHS branding no - # matter what their email domain is (but we look it up based on the - # `nhs.uk` domain to avoid hard-coding a branding ID anywhere) - ('nhs', 'test@example.voa.gov.uk', '4'), - ('nhs', 'test@nhs.uk', '4'), -]) -def test_should_lookup_branding_for_known_domain( - app_, - client_request, - active_user_with_permissions, - mock_create_service, - mock_get_services, - mock_update_service, - mock_create_or_update_free_sms_fragment_limit, - mock_get_all_email_branding, - organisation_type, - email_address, - expected_branding, -): - active_user_with_permissions.email_address = email_address - client_request.login(active_user_with_permissions) - client_request.post( - 'main.add_service', - _data={ - 'name': 'testing the post', - 'organisation_type': organisation_type, - } - ) - mock_get_all_email_branding.assert_called_once_with() - assert mock_create_service.called is True - if expected_branding: - mock_update_service.assert_called_once_with( - 101, - email_branding=expected_branding, - ) - else: - assert mock_update_service.called is False - - def test_should_return_form_errors_when_service_name_is_empty( client_request ): diff --git a/tests/conftest.py b/tests/conftest.py index 0ba53ab17..f29ffb2d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -625,7 +625,6 @@ def mock_create_service(mocker): restricted, user_id, email_from, - service_domain, ): service = service_json( 101, service_name, [user_id], message_limit=message_limit, restricted=restricted, email_from=email_from) @@ -644,7 +643,6 @@ def mock_create_duplicate_service(mocker): restricted, user_id, email_from, - service_domain, ): json_mock = Mock(return_value={'message': {'name': ["Duplicate service name '{}'".format(service_name)]}}) resp_mock = Mock(status_code=400, json=json_mock) From 835d11d8d6958ac90246d52bacb853fd5fedf2db Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 4 Apr 2019 11:17:35 +0100 Subject: [PATCH 10/31] Remove domains.yml dependence from agreement pages This will allow us to remove the `domains.yml` file, by using information about organisations that is now stored in the database instead. --- app/main/views/agreement.py | 16 +++------- tests/app/main/views/test_agreement.py | 43 ++++++++++++++------------ 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/app/main/views/agreement.py b/app/main/views/agreement.py index 3862be01c..88fedeb8c 100644 --- a/app/main/views/agreement.py +++ b/app/main/views/agreement.py @@ -1,21 +1,17 @@ from flask import abort, render_template, request, send_file, url_for -from flask_login import login_required +from flask_login import current_user, login_required from app.main import main from app.main.views.sub_navigation_dictionaries import features_nav from app.s3_client.s3_mou_client import get_mou -from app.utils import AgreementInfo @main.route('/agreement') @login_required def agreement(): - - agreement_info = AgreementInfo.from_current_user() - return render_template( - 'views/{}.html'.format(agreement_info.as_jinja_template), - owner=agreement_info.owner, + 'views/{}.html'.format(current_user.default_organisation.as_jinja_template), + owner=current_user.default_organisation.name, navigation_links=features_nav(), ) @@ -24,7 +20,7 @@ def agreement(): @login_required def download_agreement(): return send_file(**get_mou( - AgreementInfo.from_current_user().crown_status_or_404 + current_user.default_organisation.crown_status_or_404 )) @@ -40,10 +36,8 @@ def public_agreement(variant): organisation_is_crown=(variant == 'crown') )) - agreement_info = AgreementInfo.from_current_user() - return render_template( 'views/agreement-public.html', - owner=agreement_info.owner, + owner=current_user.default_organisation.name, download_link=url_for('.public_download_agreement', variant=variant), ) diff --git a/tests/app/main/views/test_agreement.py b/tests/app/main/views/test_agreement.py index 8da0fd3e2..0b3806c15 100644 --- a/tests/app/main/views/test_agreement.py +++ b/tests/app/main/views/test_agreement.py @@ -4,7 +4,7 @@ from io import BytesIO import pytest from flask import url_for -from tests.conftest import active_user_with_permissions +from tests.conftest import mock_get_organisation_by_domain class _MockS3Object(): @@ -16,22 +16,22 @@ class _MockS3Object(): return {'Body': BytesIO(self.data)} -@pytest.mark.parametrize('email_address, expected_links', [ +@pytest.mark.parametrize('agreement_signed, crown, expected_links', [ ( - 'test@cabinet-office.gov.uk', + True, True, [ partial(url_for, 'main.download_agreement'), ] ), ( - 'test@aylesburytowncouncil.gov.uk', + False, False, [ partial(url_for, 'main.download_agreement'), lambda: 'mailto:notify-support@digital.cabinet-office.gov.uk', ] ), ( - 'test@unknown.gov.uk', + None, None, [ partial(url_for, 'main.public_download_agreement', variant='crown'), partial(url_for, 'main.public_download_agreement', variant='non-crown'), @@ -44,12 +44,15 @@ def test_show_agreement_page( client_request, mocker, fake_uuid, - email_address, + agreement_signed, + crown, expected_links, ): - user = active_user_with_permissions(fake_uuid) - user.email_address = email_address - mocker.patch('app.user_api_client.get_user', return_value=user) + mock_get_organisation_by_domain( + mocker, + crown=crown, + agreement_signed=agreement_signed, + ) page = client_request.get('main.agreement') links = page.select('main .column-two-thirds a') assert len(links) == len(expected_links) @@ -57,14 +60,14 @@ def test_show_agreement_page( assert link['href'] == expected_links[index]() -@pytest.mark.parametrize('email_address, expected_file_fetched, expected_file_served', [ +@pytest.mark.parametrize('crown, expected_file_fetched, expected_file_served', [ ( - 'test@cabinet-office.gov.uk', + True, 'crown.pdf', 'GOV.UK Notify data sharing and financial agreement.pdf', ), ( - 'test@aylesburytowncouncil.gov.uk', + False, 'non-crown.pdf', 'GOV.UK Notify data sharing and financial agreement (non-crown).pdf', ), @@ -73,7 +76,7 @@ def test_downloading_agreement( logged_in_client, mocker, fake_uuid, - email_address, + crown, expected_file_fetched, expected_file_served, ): @@ -81,9 +84,10 @@ def test_downloading_agreement( 'app.s3_client.s3_mou_client.get_s3_object', return_value=_MockS3Object(b'foo') ) - user = active_user_with_permissions(fake_uuid) - user.email_address = email_address - mocker.patch('app.user_api_client.get_user', return_value=user) + mock_get_organisation_by_domain( + mocker, + crown=crown, + ) response = logged_in_client.get(url_for('main.download_agreement')) assert response.status_code == 200 assert response.get_data() == b'foo' @@ -103,9 +107,10 @@ def test_agreement_cant_be_downloaded_unknown_crown_status( 'app.s3_client.s3_mou_client.get_s3_object', return_value=_MockS3Object() ) - user = active_user_with_permissions(fake_uuid) - user.email_address = 'test@unknown.gov.uk' - mocker.patch('app.user_api_client.get_user', return_value=user) + mock_get_organisation_by_domain( + mocker, + crown=None, + ) response = logged_in_client.get(url_for('main.download_agreement')) assert response.status_code == 404 assert mock_get_s3_object.call_args_list == [] From 3565ffc33f7c17cb42ac3d01880a0bd44215d147 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 4 Apr 2019 11:23:40 +0100 Subject: [PATCH 11/31] Remove dependence on domains.yml from settings Settings looked at `domains.yml` when users were making go live requests or email branding requests. This will allow us to remove the `domains.yml` file, by using information about organisations that is now stored in the database instead. --- app/main/views/service_settings.py | 24 +++---- app/models/organisation.py | 12 ++-- app/models/service.py | 4 -- app/templates/views/service-settings.html | 2 +- tests/app/main/views/test_service_settings.py | 70 ++++++++++++------- tests/conftest.py | 14 +++- 6 files changed, 72 insertions(+), 54 deletions(-) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index ba20ccbb1..966ca1649 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -54,7 +54,6 @@ from app.main.forms import ( branding_options_dict, ) from app.utils import ( - AgreementInfo, email_safe, user_has_permissions, user_is_gov_user, @@ -180,7 +179,7 @@ def estimate_usage(service_id): @user_has_permissions('manage_service') def request_to_go_live(service_id): - agreement_signed = AgreementInfo.from_current_user().agreement_signed + agreement_signed = current_service.organisation.agreement_signed return render_template( 'views/service-settings/request-to-go-live.html', @@ -226,7 +225,7 @@ def submit_request_to_go_live(service_id): service_name=current_service.name, service_dashboard=url_for('main.service_dashboard', service_id=current_service.id, _external=True), organisation_type=str(current_service.organisation_type).title(), - agreement=AgreementInfo.from_current_user().as_human_readable, + agreement=current_service.organisation.as_human_readable(current_user.email_domain), checklist=current_service.go_live_checklist_completed_as_yes_no, volume_email=print_if_number(current_service.volume_email), volume_email_formatted=format_if_number(current_service.volume_email), @@ -237,7 +236,7 @@ def submit_request_to_go_live(service_id): research_consent='Yes' if current_service.consent_to_research else 'No', existing_live='Yes' if user_api_client.user_has_live_services(current_user) else 'No', service_id=current_service.id, - organisation=AgreementInfo.from_current_user().owner, + organisation=current_service.organisation.name, user_name=current_user.name, user_email=current_user.email_address, date=datetime.now(tz=pytz.timezone('Europe/London')).strftime('%d/%m/%Y'), @@ -245,7 +244,7 @@ def submit_request_to_go_live(service_id): ticket_type=zendesk_client.TYPE_QUESTION, user_email=current_user.email_address, user_name=current_user.name, - tags=get_request_to_go_live_tags(current_service, current_user), + tags=get_request_to_go_live_tags(current_service), ) flash('Thanks for your request to go live. We’ll get back to you within one working day.', 'default') @@ -985,7 +984,7 @@ def branding_request(service_id): '\nCurrent branding: {current_branding}' '\nBranding requested: {branding_requested}' ).format( - organisation=AgreementInfo.from_current_user().as_info_for_branding_request, + organisation=current_service.organisation.as_info_for_branding_request(current_user.email_domain), service_name=current_service.name, dashboard_url=url_for('main.service_dashboard', service_id=current_service.id, _external=True), current_branding=current_service.email_branding_name, @@ -1073,20 +1072,17 @@ def check_contact_details_type(contact_details): return 'phone_number' -def get_request_to_go_live_tags(service, user): - return list(_get_request_to_go_live_tags( - service, - AgreementInfo.from_user(user).agreement_signed, - )) +def get_request_to_go_live_tags(service): + return list(_get_request_to_go_live_tags(service)) -def _get_request_to_go_live_tags(service, agreement_signed): +def _get_request_to_go_live_tags(service): BASE = 'notify_request_to_go_live' yield BASE - if service.go_live_checklist_completed and agreement_signed: + if service.go_live_checklist_completed and service.organisation.agreement_signed: yield BASE + '_complete' return @@ -1094,7 +1090,7 @@ def _get_request_to_go_live_tags(service, agreement_signed): (True, ''), (not service.volumes, '_volumes'), (not service.go_live_checklist_completed, '_checklist'), - (not agreement_signed, '_mou'), + (not service.organisation.agreement_signed, '_mou'), (service.needs_to_add_email_reply_to_address, '_email_reply_to'), (not service.has_team_members, '_team_member'), (not service.has_templates, '_template_content'), diff --git a/app/models/organisation.py b/app/models/organisation.py index d3753a431..e3f827ff7 100644 --- a/app/models/organisation.py +++ b/app/models/organisation.py @@ -31,13 +31,12 @@ class Organisation(JSONModel): def crown_status(self): return self.crown - @property - def as_human_readable(self): + def as_human_readable(self, fallback_domain): if 'dwp.' in ''.join(self.domains): return 'DWP - Requires OED approval' if self.agreement_signed: return 'Yes, on behalf of {}'.format(self.name) - elif self.owner: + elif self.name: return '{} (organisation is {}, {})'.format( { False: 'No', @@ -51,11 +50,10 @@ class Organisation(JSONModel): }.get(self.crown_status), ) else: - return 'Can’t tell (domain is {})'.format(self._domain) + return 'Can’t tell (domain is {})'.format(fallback_domain) - @property - def as_info_for_branding_request(self): - return self.owner or 'Can’t tell (domain is {})'.format(self._domain) + def as_info_for_branding_request(self, fallback_domain): + return self.name or 'Can’t tell (domain is {})'.format(fallback_domain) @property def as_jinja_template(self): diff --git a/app/models/service.py b/app/models/service.py index 425fceb02..c28e6ff62 100644 --- a/app/models/service.py +++ b/app/models/service.py @@ -396,10 +396,6 @@ class Service(JSONModel): organisations_client.get_service_organisation(self.id) ) - @property - def organisation_name(self): - return self.organisation.name - @cached_property def inbound_number(self): return inbound_number_client.get_inbound_sms_number_for_service(self.id)['data'].get('number', '') diff --git a/app/templates/views/service-settings.html b/app/templates/views/service-settings.html index 34b725d04..0a18bde59 100644 --- a/app/templates/views/service-settings.html +++ b/app/templates/views/service-settings.html @@ -300,7 +300,7 @@ {% call row() %} {{ text_field('Organisation')}} - {{ optional_text_field(current_service.organisation_name) }} + {{ optional_text_field(current_service.organisation.name) }} {{ edit_field('Change', url_for('.link_service_to_organisation', service_id=current_service.id)) }} {% endcall %} {% call row() %} diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 847bc3536..6fcabf559 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -1,4 +1,3 @@ -from collections import namedtuple from functools import partial from unittest.mock import ANY, PropertyMock, call from urllib.parse import parse_qs, urlparse @@ -31,6 +30,7 @@ from tests.conftest import ( get_non_default_letter_contact_block, get_non_default_reply_to_email_address, get_non_default_sms_sender, + mock_get_service_organisation, multiple_letter_contact_blocks, multiple_reply_to_email_addresses, multiple_sms_senders, @@ -101,7 +101,7 @@ def mock_get_service_settings_page_common( 'Label Value Action', 'Live Off Change', 'Count in list of live services Yes Change', - 'Organisation Org 1 Change', + 'Organisation Test Organisation Change', 'Organisation type Central Change', 'Free text message allowance 250,000 Change', 'Email branding GOV.UK Change', @@ -616,6 +616,7 @@ def test_should_check_if_estimated_volumes_provided( single_reply_to_email_address, mock_get_service_templates, mock_get_users_by_service, + mock_get_service_organisation, volumes, consent_to_research, expected_estimated_volumes_item, @@ -675,6 +676,7 @@ def test_should_check_if_estimated_volumes_provided( def test_should_check_for_sending_things_right( client_request, mocker, + mock_get_service_organisation, single_sms_sender, count_of_users_with_manage_service, expected_user_checklist_item, @@ -685,7 +687,6 @@ def test_should_check_for_sending_things_right( reply_to_email_addresses, expected_reply_to_checklist_item, ): - def _templates_by_type(template_type): return { 'email': list(range(0, count_of_email_templates)), @@ -751,25 +752,22 @@ def test_should_not_show_go_live_button_if_checklist_not_complete( mocker, mock_get_service_templates, mock_get_users_by_service, + mock_get_service_organisation, single_sms_sender, checklist_completed, agreement_signed, expected_button, ): - - def _agreement_info(): - return namedtuple( - 'AgreementInfo', ['agreement_signed'] - )(agreement_signed=agreement_signed) - mocker.patch( 'app.models.service.Service.go_live_checklist_completed', new_callable=PropertyMock, return_value=checklist_completed, ) mocker.patch( - 'app.utils.AgreementInfo.from_current_user', - side_effect=_agreement_info, + 'app.models.organisation.Organisation.agreement_signed', + new_callable=PropertyMock, + return_value=agreement_signed, + create=True, ) for channel in ('email', 'sms', 'letter'): @@ -894,13 +892,13 @@ def test_should_check_for_sms_sender_on_go_live( client_request, service_one, mocker, + mock_get_service_organisation, organisation_type, count_of_sms_templates, sms_senders, expected_sms_sender_checklist_item, estimated_sms_volume, ): - service_one['organisation_type'] = organisation_type def _templates_by_type(template_type): @@ -953,18 +951,18 @@ def test_should_check_for_sms_sender_on_go_live( mock_get_sms_senders.assert_called_once_with(SERVICE_ONE_ID) -@pytest.mark.parametrize('email_address, expected_item', ( +@pytest.mark.parametrize('agreement_signed, expected_item', ( pytest.param( - 'test@unknown.gov.uk', + None, '', marks=pytest.mark.xfail(raises=IndexError) ), ( - 'test@education.gov.uk', + True, 'Sign our data sharing and financial agreement Completed', ), ( - 'test@aylesbury.gov.uk', + False, 'Sign our data sharing and financial agreement Not completed', ), )) @@ -972,7 +970,7 @@ def test_should_check_for_mou_on_request_to_go_live( client_request, service_one, mocker, - email_address, + agreement_signed, expected_item, ): mocker.patch( @@ -1000,9 +998,10 @@ def test_should_check_for_mou_on_request_to_go_live( return_value=None, ) - user = active_user_with_permissions(uuid4()) - user.email_address = email_address - client_request.login(user) + mock_get_service_organisation( + mocker, + agreement_signed=agreement_signed, + ) page = client_request.get( 'main.request_to_go_live', service_id=SERVICE_ONE_ID @@ -1017,6 +1016,7 @@ def test_non_gov_user_is_told_they_cant_go_live( client_request, api_nongov_user_active, mocker, + mock_get_service_organisation, ): mocker.patch( 'app.main.views.service_settings.user_api_client.get_count_of_users_with_permission', @@ -1287,7 +1287,6 @@ def test_should_redirect_after_request_to_go_live( active_user_with_permissions, single_reply_to_email_address, single_letter_contact_block, - mock_get_service_organisation, mock_get_organisations_and_services_for_user, single_sms_sender, mock_get_service_settings_page_common, @@ -1298,6 +1297,11 @@ def test_should_redirect_after_request_to_go_live( formatted_displayed_volumes, extra_tags, ): + mock_get_service_organisation( + mocker, + name=None, + agreement_signed=None, + ) for channel, volume in volumes: mocker.patch( 'app.models.service.Service.volume_{}'.format(channel), @@ -1516,6 +1520,11 @@ def test_ready_to_go_live( agreement_signed, expected_tags, ): + mock_get_service_organisation( + mocker, + agreement_signed=agreement_signed, + ) + for prop in { 'has_team_members', 'has_templates', @@ -1547,8 +1556,7 @@ def test_ready_to_go_live( }).go_live_checklist_completed_as_yes_no == expected_readyness assert list(app.main.views.service_settings._get_request_to_go_live_tags( - app.models.service.Service({'id': SERVICE_ONE_ID}), - agreement_signed, + app.models.service.Service({'id': SERVICE_ONE_ID}) )) == expected_tags @@ -3966,6 +3974,10 @@ def test_show_email_branding_request_page_when_email_branding_is_set( ('org_banner', 'Your logo on a colour'), pytest.param('foo', 'Nope', marks=pytest.mark.xfail(raises=AssertionError)), )) +@pytest.mark.parametrize('org_name, expected_organisation', ( + (None, 'Can’t tell (domain is user.gov.uk)'), + ('Test Organisation', 'Test Organisation'), +)) def test_submit_email_branding_request( client_request, mocker, @@ -3974,10 +3986,16 @@ def test_submit_email_branding_request( mock_get_service_settings_page_common, no_reply_to_email_addresses, no_letter_contact_blocks, - mock_get_service_organisation, single_sms_sender, + org_name, + expected_organisation, ): + mock_get_service_organisation( + mocker, + name=org_name, + ) + zendesk = mocker.patch( 'app.main.views.service_settings.zendesk_client.create_ticket', autospec=True, @@ -3993,14 +4011,14 @@ def test_submit_email_branding_request( zendesk.assert_called_once_with( message='\n'.join([ - 'Organisation: Can’t tell (domain is user.gov.uk)', + 'Organisation: {}', 'Service: service one', 'http://localhost/services/596364a0-858e-42c8-9062-a8fe822260eb', '', '---', 'Current branding: GOV.UK', 'Branding requested: {}', - ]).format(requested_branding), + ]).format(expected_organisation, requested_branding), subject='Email branding request - service one', ticket_type='question', user_email='test@user.gov.uk', diff --git a/tests/conftest.py b/tests/conftest.py index f29ffb2d2..21274f0ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3145,9 +3145,19 @@ def mock_get_organisation_by_domain( @pytest.fixture(scope='function') -def mock_get_service_organisation(mocker): +def mock_get_service_organisation( + mocker, + name=False, + crown=True, + agreement_signed=None, +): def _get_service_organisation(service_id): - return organisation_json('7aa5d4e9-4385-4488-a489-07812ba13383', 'Org 1') + return organisation_json( + '7aa5d4e9-4385-4488-a489-07812ba13383', + name, + crown=crown, + agreement_signed=agreement_signed, + ) return mocker.patch('app.organisations_client.get_service_organisation', side_effect=_get_service_organisation) From 9684e962ae4850bb4d954beb0db2ee5c8f69eeff Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 4 Apr 2019 11:27:05 +0100 Subject: [PATCH 12/31] Refactor go live tags into service model Since this function only takes one argument, a service, it might as well be a method of the service. --- app/main/views/service_settings.py | 30 +------------------ app/models/service.py | 27 +++++++++++++++++ tests/app/main/views/test_service_settings.py | 6 ++-- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index 966ca1649..409b9ad2e 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -244,7 +244,7 @@ def submit_request_to_go_live(service_id): ticket_type=zendesk_client.TYPE_QUESTION, user_email=current_user.email_address, user_name=current_user.name, - tags=get_request_to_go_live_tags(current_service), + tags=current_service.request_to_go_live_tags, ) flash('Thanks for your request to go live. We’ll get back to you within one working day.', 'default') @@ -1072,34 +1072,6 @@ def check_contact_details_type(contact_details): return 'phone_number' -def get_request_to_go_live_tags(service): - return list(_get_request_to_go_live_tags(service)) - - -def _get_request_to_go_live_tags(service): - - BASE = 'notify_request_to_go_live' - - yield BASE - - if service.go_live_checklist_completed and service.organisation.agreement_signed: - yield BASE + '_complete' - return - - for test, tag in ( - (True, ''), - (not service.volumes, '_volumes'), - (not service.go_live_checklist_completed, '_checklist'), - (not service.organisation.agreement_signed, '_mou'), - (service.needs_to_add_email_reply_to_address, '_email_reply_to'), - (not service.has_team_members, '_team_member'), - (not service.has_templates, '_template_content'), - (service.needs_to_change_sms_sender, '_sms_sender'), - ): - if test: - yield BASE + '_incomplete' + tag - - def print_if_number(value): return value if isinstance(value, int) else '' diff --git a/app/models/service.py b/app/models/service.py index c28e6ff62..c0b1b5e6b 100644 --- a/app/models/service.py +++ b/app/models/service.py @@ -543,3 +543,30 @@ class Service(JSONModel): def get_api_key(self, id): return self._get_by_id(self.api_keys, id) + + @property + def request_to_go_live_tags(self): + return list(self._get_request_to_go_live_tags()) + + def _get_request_to_go_live_tags(self): + + BASE = 'notify_request_to_go_live' + + yield BASE + + if self.go_live_checklist_completed and self.organisation.agreement_signed: + yield BASE + '_complete' + return + + for test, tag in ( + (True, ''), + (not self.volumes, '_volumes'), + (not self.go_live_checklist_completed, '_checklist'), + (not self.organisation.agreement_signed, '_mou'), + (self.needs_to_add_email_reply_to_address, '_email_reply_to'), + (not self.has_team_members, '_team_member'), + (not self.has_templates, '_template_content'), + (self.needs_to_change_sms_sender, '_sms_sender'), + ): + if test: + yield BASE + '_incomplete' + tag diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 6fcabf559..78d25e59c 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -1555,9 +1555,9 @@ def test_ready_to_go_live( 'id': SERVICE_ONE_ID }).go_live_checklist_completed_as_yes_no == expected_readyness - assert list(app.main.views.service_settings._get_request_to_go_live_tags( - app.models.service.Service({'id': SERVICE_ONE_ID}) - )) == expected_tags + assert app.models.service.Service( + {'id': SERVICE_ONE_ID} + ).request_to_go_live_tags == expected_tags @pytest.mark.parametrize('route', [ From 470b8a2912c4c42318a7bc199d094825acc2e92b Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 4 Apr 2019 11:09:09 +0100 Subject: [PATCH 13/31] Remove domains from branding forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’re deprecating storing the domain as text on a branding in favour of a database relationship between branding and organisation. We need to do this now in order to remove the validation on these fields (which depends on the data in `domains.yml`) --- app/main/forms.py | 19 +-- app/main/validators.py | 33 +---- app/main/views/email_branding.py | 3 - app/main/views/letter_branding.py | 13 +- app/notify_client/email_branding_client.py | 12 +- app/notify_client/letter_branding_client.py | 7 +- .../views/email-branding/manage-branding.html | 1 - .../views/email-branding/select-branding.html | 7 -- .../manage-letter-branding.html | 1 - .../select-letter-branding.html | 7 -- tests/app/main/views/test_email_branding.py | 119 ++---------------- tests/app/main/views/test_letter_branding.py | 95 ++++---------- .../test_email_branding_client.py | 8 +- .../test_letter_branding_client.py | 8 +- tests/conftest.py | 13 +- 15 files changed, 48 insertions(+), 298 deletions(-) diff --git a/app/main/forms.py b/app/main/forms.py index 5878b129c..184f65d5f 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -36,10 +36,8 @@ from wtforms.widgets import CheckboxInput, ListWidget from app.main.validators import ( Blacklist, - CanonicalGovernmentDomain, CsvFileValidator, DoesNotStartWithDoubleZero, - KnownGovernmentDomain, LettersNumbersAndFullStopsOnly, NoCommasInPlaceHolders, OnlyGSMCharacters, @@ -47,7 +45,7 @@ from app.main.validators import ( ValidGovEmail, ) from app.models.user import permissions, roles -from app.utils import AgreementInfo, guess_name_from_email_address +from app.utils import guess_name_from_email_address def get_time_value_and_label(future_time): @@ -990,23 +988,9 @@ class PreviewBranding(StripWhitespaceForm): branding_style = HiddenFieldWithNoneOption('branding_style') -class GovernmentDomainField(StringField): - validators = [ - KnownGovernmentDomain(), - CanonicalGovernmentDomain(), - ] - - def post_validate(self, form, validation_stopped): - if self.data == '': - self.data = None - if self.data and not self.errors: - self.data = AgreementInfo(self.data).canonical_domain - - class ServiceUpdateEmailBranding(StripWhitespaceForm): name = StringField('Name of brand') text = StringField('Text') - domain = GovernmentDomainField('Domain') colour = StringField( 'Colour', validators=[ @@ -1041,7 +1025,6 @@ class SVGFileUpload(StripWhitespaceForm): class ServiceLetterBrandingDetails(StripWhitespaceForm): name = StringField('Name of brand', validators=[DataRequired()]) - domain = GovernmentDomainField('Domain') class PDFUploadForm(StripWhitespaceForm): diff --git a/app/main/validators.py b/app/main/validators.py index 9b27e09e9..216038726 100644 --- a/app/main/validators.py +++ b/app/main/validators.py @@ -11,7 +11,7 @@ from wtforms.validators import Email from app import formatted_list from app.main._blacklisted_passwords import blacklisted_passwords -from app.utils import AgreementInfo, Spreadsheet, is_gov_user +from app.utils import Spreadsheet, is_gov_user class Blacklist: @@ -111,34 +111,3 @@ class DoesNotStartWithDoubleZero: def __call__(self, form, field): if field.data and field.data.startswith("00"): raise ValidationError(self.message) - - -class KnownGovernmentDomain: - - message = 'Not a known government domain (you might need to update domains.yml)' - - def __call__(self, form, field): - if field.data and AgreementInfo(field.data).owner is None: - raise ValidationError(self.message) - - -class CanonicalGovernmentDomain: - - message = 'Not {} domain (use {} if appropriate)' - - def __call__(self, form, field): - - if not field.data: - return - - domain = AgreementInfo(field.data) - - if not domain.is_canonical: - raise ValidationError( - self.message.format('a canonical', domain.canonical_domain) - ) - - if field.data != domain.canonical_domain: - raise ValidationError( - self.message.format('an organisation-level', domain.canonical_domain) - ) diff --git a/app/main/views/email_branding.py b/app/main/views/email_branding.py index 7fbb9aec8..521b7b1e8 100644 --- a/app/main/views/email_branding.py +++ b/app/main/views/email_branding.py @@ -39,7 +39,6 @@ def update_email_branding(branding_id, logo=None): name=email_branding['name'], text=email_branding['text'], colour=email_branding['colour'], - domain=email_branding['domain'], brand_type=email_branding['brand_type'] ) @@ -67,7 +66,6 @@ def update_email_branding(branding_id, logo=None): name=form.name.data, text=form.text.data, colour=form.colour.data, - domain=form.domain.data, brand_type=form.brand_type.data, ) @@ -115,7 +113,6 @@ def create_email_branding(logo=None): name=form.name.data, text=form.text.data, colour=form.colour.data, - domain=form.domain.data, brand_type=form.brand_type.data, ) diff --git a/app/main/views/letter_branding.py b/app/main/views/letter_branding.py index c2a126878..634dfa777 100644 --- a/app/main/views/letter_branding.py +++ b/app/main/views/letter_branding.py @@ -56,7 +56,6 @@ def update_letter_branding(branding_id, logo=None): file_upload_form = SVGFileUpload() letter_branding_details_form = ServiceLetterBrandingDetails( name=letter_branding['name'], - domain=letter_branding['domain'] ) file_upload_form_submitted = file_upload_form.file.data @@ -87,7 +86,6 @@ def update_letter_branding(branding_id, logo=None): branding_id=branding_id, filename=db_filename, name=letter_branding_details_form.name.data, - domain=letter_branding_details_form.domain.data ) return redirect(url_for('main.letter_branding')) @@ -98,7 +96,6 @@ def update_letter_branding(branding_id, logo=None): branding_id=branding_id, filename=db_filename, name=letter_branding_details_form.name.data, - domain=letter_branding_details_form.domain.data ) upload_letter_logos(logo, db_filename, png_file, session['user_id']) @@ -106,9 +103,7 @@ def update_letter_branding(branding_id, logo=None): return redirect(url_for('main.letter_branding')) except HTTPError as e: - if 'domain' in e.message: - letter_branding_details_form.domain.errors.append(e.message['domain'][0]) - elif 'name' in e.message: + if 'name' in e.message: letter_branding_details_form.name.errors.append(e.message['name'][0]) else: raise e @@ -118,7 +113,6 @@ def update_letter_branding(branding_id, logo=None): branding_id=branding_id, filename=letter_branding['filename'], name=letter_branding['name'], - domain=letter_branding['domain'] ) file_upload_form.file.errors = ['Error saving uploaded file - try uploading again'] @@ -165,7 +159,6 @@ def create_letter_branding(logo=None): letter_branding_client.create_letter_branding( filename=db_filename, name=letter_branding_details_form.name.data, - domain=letter_branding_details_form.domain.data, ) upload_letter_logos(logo, db_filename, png_file, session['user_id']) @@ -173,9 +166,7 @@ def create_letter_branding(logo=None): return redirect(url_for('main.letter_branding')) except HTTPError as e: - if 'domain' in e.message: - letter_branding_details_form.domain.errors.append(e.message['domain'][0]) - elif 'name' in e.message: + if 'name' in e.message: letter_branding_details_form.name.errors.append(e.message['name'][0]) else: raise e diff --git a/app/notify_client/email_branding_client.py b/app/notify_client/email_branding_client.py index 776c0d1e6..469e6f7c5 100644 --- a/app/notify_client/email_branding_client.py +++ b/app/notify_client/email_branding_client.py @@ -14,33 +14,25 @@ class EmailBrandingClient(NotifyAdminAPIClient): brandings.sort(key=lambda branding: branding[sort_key].lower()) return brandings - def get_email_branding_id_for_domain(self, domain): - for branding in self.get_all_email_branding(): - if domain and branding.get('domain') == domain: - return branding['id'] - return None - @cache.delete('email_branding') - def create_email_branding(self, logo, name, text, colour, domain, brand_type): + def create_email_branding(self, logo, name, text, colour, brand_type): data = { "logo": logo, "name": name, "text": text, "colour": colour, - "domain": domain, "brand_type": brand_type } return self.post(url="/email-branding", data=data) @cache.delete('email_branding') @cache.delete('email_branding-{branding_id}') - def update_email_branding(self, branding_id, logo, name, text, colour, domain, brand_type): + def update_email_branding(self, branding_id, logo, name, text, colour, brand_type): data = { "logo": logo, "name": name, "text": text, "colour": colour, - "domain": domain, "brand_type": brand_type } return self.post(url="/email-branding/{}".format(branding_id), data=data) diff --git a/app/notify_client/letter_branding_client.py b/app/notify_client/letter_branding_client.py index 0a577d55e..736d9d025 100644 --- a/app/notify_client/letter_branding_client.py +++ b/app/notify_client/letter_branding_client.py @@ -12,22 +12,19 @@ class LetterBrandingClient(NotifyAdminAPIClient): return self.get(url='/letter-branding') @cache.delete('letter_branding') - def create_letter_branding(self, filename, name, domain): + def create_letter_branding(self, filename, name): data = { "filename": filename, "name": name, - "domain": domain, } return self.post(url="/letter-branding", data=data) @cache.delete('letter_branding') @cache.delete('letter_branding-{branding_id}') - def update_letter_branding(self, branding_id, filename, name, domain): + def update_letter_branding(self, branding_id, filename, name): data = { "filename": filename, "name": name, - "domain": domain, - } return self.post(url="/letter-branding/{}".format(branding_id), data=data) diff --git a/app/templates/views/email-branding/manage-branding.html b/app/templates/views/email-branding/manage-branding.html index dad8e212d..42d695786 100644 --- a/app/templates/views/email-branding/manage-branding.html +++ b/app/templates/views/email-branding/manage-branding.html @@ -28,7 +28,6 @@

{{textbox(form.name)}}
{{textbox(form.text)}}
{{ textbox(form.colour, width='1-4', colour_preview=True) }} -
{{textbox(form.domain)}}
{{ radios(form.brand_type) }} {{ page_footer( 'Save', diff --git a/app/templates/views/email-branding/select-branding.html b/app/templates/views/email-branding/select-branding.html index 2d9f703f7..27c8086ef 100644 --- a/app/templates/views/email-branding/select-branding.html +++ b/app/templates/views/email-branding/select-branding.html @@ -24,13 +24,6 @@ {{ brand.name or 'Unnamed' }}
-

- {% if brand.domain %} - Default for {{ brand.domain }} - {% else %} - – - {% endif %} -

{% endfor %} diff --git a/app/templates/views/letter-branding/manage-letter-branding.html b/app/templates/views/letter-branding/manage-letter-branding.html index 1dd6c7b8a..e815d1057 100644 --- a/app/templates/views/letter-branding/manage-letter-branding.html +++ b/app/templates/views/letter-branding/manage-letter-branding.html @@ -25,7 +25,6 @@ {% call form_wrapper() %}
{{textbox(letter_branding_details_form.name)}}
-
{{textbox(letter_branding_details_form.domain)}}
{{ page_footer( 'Save', button_name='operation', diff --git a/app/templates/views/letter-branding/select-letter-branding.html b/app/templates/views/letter-branding/select-letter-branding.html index b915c3b73..3b50744f5 100644 --- a/app/templates/views/letter-branding/select-letter-branding.html +++ b/app/templates/views/letter-branding/select-letter-branding.html @@ -24,13 +24,6 @@ {{ brand.name }}
-

- {% if brand.domain %} - Default for {{ brand.domain }} - {% else %} - – - {% endif %} -

{% endfor %} diff --git a/tests/app/main/views/test_email_branding.py b/tests/app/main/views/test_email_branding.py index be702a3bd..e49f8ecef 100644 --- a/tests/app/main/views/test_email_branding.py +++ b/tests/app/main/views/test_email_branding.py @@ -28,7 +28,6 @@ def test_email_branding_page_shows_full_branding_list( links = page.select('.message-name a') brand_names = [normalize_spaces(link.text) for link in links] hrefs = [link['href'] for link in links] - brand_hints = [normalize_spaces(hint.text) for hint in page.select('.message-type')] assert normalize_spaces( page.select_one('h1').text @@ -36,14 +35,12 @@ def test_email_branding_page_shows_full_branding_list( assert page.select_one('.column-three-quarters a')['href'] == url_for('main.create_email_branding') - assert list(zip( - brand_names, brand_hints - )) == [ - ('org 1', '–'), - ('org 2', '–'), - ('org 3', '–'), - ('org 4', 'Default for nhs.uk'), - ('org 5', 'Default for voa.gov.uk'), + assert brand_names == [ + 'org 1', + 'org 2', + 'org 3', + 'org 4', + 'org 5', ] assert hrefs == [ url_for('.update_email_branding', branding_id=1), @@ -70,7 +67,6 @@ def test_edit_email_branding_shows_the_correct_branding_info( assert page.select_one('#name').attrs.get('value') == 'Organisation name' assert page.select_one('#text').attrs.get('value') == 'Organisation text' assert page.select_one('#colour').attrs.get('value') == '#f00' - assert page.select_one('#domain').attrs.get('value') == 'sample.com' def test_create_email_branding_does_not_show_any_branding_info( @@ -89,27 +85,19 @@ def test_create_email_branding_does_not_show_any_branding_info( assert page.select_one('#name').attrs.get('value') == '' assert page.select_one('#text').attrs.get('value') == '' assert page.select_one('#colour').attrs.get('value') == '' - assert page.select_one('#domain').attrs.get('value') == '' -@pytest.mark.parametrize('posted_domain, persisted_domain', [ - ('voa.gov.uk', 'voa.gov.uk'), - ('', None), -]) def test_create_new_email_branding_without_logo( logged_in_platform_admin_client, mocker, fake_uuid, mock_create_email_branding, - posted_domain, - persisted_domain, ): data = { 'logo': None, 'colour': '#ff0000', 'text': 'new text', 'name': 'new name', - 'domain': posted_domain, 'brand_type': 'org' } @@ -128,96 +116,11 @@ def test_create_new_email_branding_without_logo( name=data['name'], text=data['text'], colour=data['colour'], - domain=persisted_domain, brand_type=data['brand_type'] ) assert mock_persist.call_args_list == [] -def test_cant_create_new_email_branding_with_unknown_domain( - client_request, - mocker, - fake_uuid, - mock_create_email_branding -): - mock_persist = mocker.patch('app.main.views.email_branding.persist_logo') - mocker.patch('app.main.views.email_branding.delete_email_temp_files_created_by') - - client_request.login(platform_admin_user(fake_uuid)) - page = client_request.post( - '.create_email_branding', - content_type='multipart/form-data', - _data={ - 'logo': None, - 'colour': '#ff0000', - 'text': 'new text', - 'name': 'new name', - 'domain': 'example.gov.uk', - 'brand_type': 'org', - }, - _expected_status=200, - ) - - assert mock_create_email_branding.called is False - assert mock_persist.called is False - - assert page.select_one('.error-message').text.strip() == ( - 'Not a known government domain (you might need to update domains.yml)' - ) - assert page.select_one('input[name=domain]')['value'] == ( - 'example.gov.uk' - ) - - -@pytest.mark.parametrize('posted_domain, expected_error', [ - ( - 'voa.gsi.gov.uk', - 'Not a canonical domain (use voa.gov.uk if appropriate)', - ), - ( - 'hmcts.net', - 'Not a canonical domain (use hmcts.gov.uk if appropriate)', - ), - ( - 'southend.essex.gov.uk', - 'Not an organisation-level domain (use essex.gov.uk if appropriate)', - ), - pytest.param( - 'voa.gov.uk', - '', - marks=pytest.mark.xfail(raises=AssertionError) - ), -]) -def test_rejects_non_canonical_domain_when_adding_email_branding( - client_request, - mocker, - fake_uuid, - mock_create_email_branding, - posted_domain, - expected_error, -): - mocker.patch('app.main.views.email_branding.persist_logo') - mocker.patch('app.main.views.email_branding.delete_email_temp_files_created_by') - data = { - 'logo': None, - 'colour': '#ff0000', - 'text': 'new text', - 'name': 'new name', - 'domain': posted_domain, - 'brand_type': 'org', - } - client_request.login(platform_admin_user(fake_uuid)) - page = client_request.post( - '.create_email_branding', - content_type='multipart/form-data', - _data=data, - _expected_status=200, - ) - - assert page.select_one('.error-message').text.strip() == expected_error - assert mock_create_email_branding.called is False - - def test_create_email_branding_requires_a_name_when_submitting_logo_details( client_request, mocker, @@ -232,7 +135,6 @@ def test_create_email_branding_requires_a_name_when_submitting_logo_details( 'colour': '#ff0000', 'text': 'new text', 'name': '', - 'domain': '', 'brand_type': 'org', } client_request.login(platform_admin_user(fake_uuid)) @@ -258,7 +160,6 @@ def test_create_email_branding_does_not_require_a_name_when_uploading_a_file( 'colour': '', 'text': '', 'name': '', - 'domain': '', 'brand_type': 'org', } client_request.login(platform_admin_user(fake_uuid)) @@ -286,7 +187,6 @@ def test_create_new_email_branding_when_branding_saved( 'colour': '#ff0000', 'text': 'new text', 'name': 'new name', - 'domain': 'voa.gov.uk', 'brand_type': 'org_banner' } @@ -307,7 +207,6 @@ def test_create_new_email_branding_when_branding_saved( 'name': data['name'], 'text': data['text'], 'cdn_url': 'https://static-logos.cdn.com', - 'domain': data['domain'], 'brand_type': data['brand_type'] } ) @@ -320,7 +219,6 @@ def test_create_new_email_branding_when_branding_saved( name=data['name'], text=data['text'], colour=data['colour'], - domain=data['domain'], brand_type=data['brand_type'] ) @@ -387,7 +285,6 @@ def test_update_existing_branding( 'colour': '#0000ff', 'text': 'new text', 'name': 'new name', - 'domain': 'voa.gov.uk', 'brand_type': 'both' } @@ -405,7 +302,7 @@ def test_update_existing_branding( content_type='multipart/form-data', data={'colour': data['colour'], 'name': data['name'], 'text': data['text'], 'cdn_url': 'https://static-logos.cdn.com', - 'domain': data['domain'], 'brand_type': data['brand_type'] + 'brand_type': data['brand_type'] } ) @@ -418,7 +315,6 @@ def test_update_existing_branding( name=data['name'], text=data['text'], colour=data['colour'], - domain=data['domain'], brand_type=data['brand_type'] ) @@ -526,7 +422,6 @@ def test_colour_regex_validation( 'colour': colour_hex, 'text': 'new text', 'name': 'new name', - 'domain': 'voa.gov.uk', 'brand_type': 'org' } diff --git a/tests/app/main/views/test_letter_branding.py b/tests/app/main/views/test_letter_branding.py index 2520d16b0..89ef8bc43 100644 --- a/tests/app/main/views/test_letter_branding.py +++ b/tests/app/main/views/test_letter_branding.py @@ -2,7 +2,6 @@ from io import BytesIO from unittest.mock import Mock, call from uuid import UUID -import pytest from botocore.exceptions import ClientError as BotoClientError from bs4 import BeautifulSoup from flask import current_app, url_for @@ -29,7 +28,6 @@ def test_letter_branding_page_shows_full_branding_list( links = page.select('.message-name a') brand_names = [normalize_spaces(link.text) for link in links] hrefs = [link['href'] for link in links] - brand_hints = [normalize_spaces(hint.text) for hint in page.select('.message-type')] assert normalize_spaces( page.select_one('h1').text @@ -37,12 +35,10 @@ def test_letter_branding_page_shows_full_branding_list( assert page.select_one('.column-three-quarters a')['href'] == url_for('main.create_letter_branding') - assert list(zip( - brand_names, brand_hints - )) == [ - ('HM Government', '–'), - ('Land Registry', 'Default for landregistry.gov.uk'), - ('Animal and Plant Health Agency', '–'), + assert brand_names == [ + 'HM Government', + 'Land Registry', + 'Animal and Plant Health Agency', ] assert hrefs == [ @@ -66,7 +62,6 @@ def test_update_letter_branding_shows_the_current_letter_brand( assert page.find('h1').text == 'Update letter branding' assert page.select_one('#logo-img > img')['src'].endswith('/hm-government.svg') assert page.select_one('#name').attrs.get('value') == 'HM Government' - assert page.select_one('#domain').attrs.get('value') == 'cabinet-office.gov.uk' def test_update_letter_branding_with_new_valid_file( @@ -95,7 +90,6 @@ def test_update_letter_branding_with_new_valid_file( assert page.select_one('#logo-img > img')['src'].endswith(expected_temp_filename) assert page.select_one('#name').attrs.get('value') == 'HM Government' - assert page.select_one('#domain').attrs.get('value') == 'cabinet-office.gov.uk' assert mock_s3_upload.called mock_delete_temp_files.assert_not_called() @@ -161,7 +155,6 @@ def test_update_letter_branding_with_original_file_and_new_details( url_for('.update_letter_branding', branding_id=fake_uuid), data={ 'name': 'Updated name', - 'domain': 'bl.uk', 'operation': 'branding-details' }, follow_redirects=True @@ -175,42 +168,12 @@ def test_update_letter_branding_with_original_file_and_new_details( mock_client_update.assert_called_once_with( branding_id=fake_uuid, - domain='bl.uk', filename='hm-government', name='Updated name' ) -def test_update_letter_branding_does_not_require_a_domain( - mocker, - logged_in_platform_admin_client, - mock_get_all_letter_branding, - mock_get_letter_branding_by_id, - fake_uuid -): - mock_client_update = mocker.patch('app.main.views.letter_branding.letter_branding_client.update_letter_branding') - logo = permanent_letter_logo_name('hm-government', 'svg') - - response = logged_in_platform_admin_client.post( - url_for('.update_letter_branding', branding_id=fake_uuid, logo=logo), - data={ - 'name': 'Updated name', - 'domain': '', - 'operation': 'branding-details' - }, - follow_redirects=True - ) - assert response.status_code == 200 - - mock_client_update.assert_called_once_with( - branding_id=fake_uuid, - domain=None, - filename='hm-government', - name='Updated name' - ) - - -def test_update_letter_branding_shows_form_errors_on_name_and_domain_fields( +def test_update_letter_branding_shows_form_errors_on_name_fields( mocker, logged_in_platform_admin_client, mock_get_letter_branding_by_id, @@ -224,7 +187,6 @@ def test_update_letter_branding_shows_form_errors_on_name_and_domain_fields( url_for('.update_letter_branding', branding_id=fake_uuid, logo=logo), data={ 'name': '', - 'domain': 'example.com', 'operation': 'branding-details' }, follow_redirects=True @@ -234,18 +196,15 @@ def test_update_letter_branding_shows_form_errors_on_name_and_domain_fields( error_messages = page.find_all('span', class_='error-message') assert page.find('h1').text == 'Update letter branding' - assert len(error_messages) == 2 + assert len(error_messages) == 1 assert error_messages[0].text.strip() == 'This field is required.' - assert error_messages[1].text.strip() == 'Not a known government domain (you might need to update domains.yml)' -@pytest.mark.parametrize('error_field', ['name', 'domain']) -def test_update_letter_branding_shows_database_errors_on_name_and_domain_fields( +def test_update_letter_branding_shows_database_errors_on_name_field( mocker, logged_in_platform_admin_client, mock_get_letter_branding_by_id, fake_uuid, - error_field ): mocker.patch('app.main.views.letter_branding.get_png_file_from_svg') mocker.patch('app.main.views.letter_branding.letter_branding_client.update_letter_branding', side_effect=HTTPError( @@ -254,20 +213,19 @@ def test_update_letter_branding_shows_database_errors_on_name_and_domain_fields( json={ 'result': 'error', 'message': { - error_field: { - '{} already in use'.format(error_field) + 'name': { + 'name already in use' } } } ), - message={error_field: ['{} already in use'.format(error_field)]} + message={'name': ['name already in use']} )) response = logged_in_platform_admin_client.post( url_for('.update_letter_branding', branding_id='abc'), data={ 'name': 'my brand', - 'domain': None, 'operation': 'branding-details' } ) @@ -276,7 +234,7 @@ def test_update_letter_branding_shows_database_errors_on_name_and_domain_fields( error_message = page.find('span', class_='error-message').text.strip() assert page.find('h1').text == 'Update letter branding' - assert error_message == '{} already in use'.format(error_field) + assert error_message == 'name already in use' def test_update_letter_branding_with_new_file_and_new_details( @@ -300,7 +258,6 @@ def test_update_letter_branding_with_new_file_and_new_details( url_for('.update_letter_branding', branding_id=fake_uuid, logo=temp_logo), data={ 'name': 'Updated name', - 'domain': 'bl.uk', 'operation': 'branding-details' }, follow_redirects=True @@ -312,7 +269,6 @@ def test_update_letter_branding_with_new_file_and_new_details( assert mock_template_preview.called mock_client_update.assert_called_once_with( branding_id=fake_uuid, - domain='bl.uk', filename='{}-new_file'.format(fake_uuid), name='Updated name' ) @@ -343,7 +299,6 @@ def test_update_letter_branding_rolls_back_db_changes_and_shows_error_if_saving_ url_for('.update_letter_branding', branding_id=fake_uuid, logo=temp_logo), data={ 'name': 'Updated name', - 'domain': 'bl.uk', 'operation': 'branding-details' }, follow_redirects=True @@ -355,8 +310,8 @@ def test_update_letter_branding_rolls_back_db_changes_and_shows_error_if_saving_ assert mock_client_update.call_count == 2 assert mock_client_update.call_args_list == [ - call(branding_id=fake_uuid, domain='bl.uk', filename='{}-new_file'.format(fake_uuid), name='Updated name'), - call(branding_id=fake_uuid, domain='cabinet-office.gov.uk', filename='hm-government', name='HM Government') + call(branding_id=fake_uuid, filename='{}-new_file'.format(fake_uuid), name='Updated name'), + call(branding_id=fake_uuid, filename='hm-government', name='HM Government') ] @@ -370,7 +325,6 @@ def test_create_letter_branding_does_not_show_branding_info(logged_in_platform_a assert page.select_one('#logo-img > img') is None assert page.select_one('#name').attrs.get('value') == '' - assert page.select_one('#domain').attrs.get('value') == '' def test_create_letter_branding_when_uploading_valid_file( @@ -472,7 +426,6 @@ def test_create_letter_branding_shows_an_error_when_submitting_details_with_no_l url_for('.create_letter_branding'), data={ 'name': 'Test brand', - 'domain': 'bl.uk', 'operation': 'branding-details' } ) @@ -506,7 +459,6 @@ def test_create_letter_branding_persists_logo_when_all_data_is_valid( url_for('.create_letter_branding', logo=temp_logo), data={ 'name': 'Test brand', - 'domain': 'bl.uk', 'operation': 'branding-details' }, follow_redirects=True @@ -517,7 +469,7 @@ def test_create_letter_branding_persists_logo_when_all_data_is_valid( assert page.find('h1').text == 'Letter branding' mock_letter_client.create_letter_branding.assert_called_once_with( - domain='bl.uk', filename='{}-test'.format(fake_uuid), name='Test brand' + filename='{}-test'.format(fake_uuid), name='Test brand' ) assert mock_template_preview.called mock_persist_logo.assert_called_once_with( @@ -532,7 +484,7 @@ def test_create_letter_branding_persists_logo_when_all_data_is_valid( mock_delete_temp_files.assert_called_once_with(user_id) -def test_create_letter_branding_shows_form_errors_on_name_and_domain_fields( +def test_create_letter_branding_shows_form_errors_on_name_field( logged_in_platform_admin_client, fake_uuid ): @@ -545,7 +497,6 @@ def test_create_letter_branding_shows_form_errors_on_name_and_domain_fields( url_for('.create_letter_branding', logo=temp_logo), data={ 'name': '', - 'domain': 'example.com', 'operation': 'branding-details' } ) @@ -554,17 +505,14 @@ def test_create_letter_branding_shows_form_errors_on_name_and_domain_fields( error_messages = page.find_all('span', class_='error-message') assert page.find('h1').text == 'Add letter branding' - assert len(error_messages) == 2 + assert len(error_messages) == 1 assert error_messages[0].text.strip() == 'This field is required.' - assert error_messages[1].text.strip() == 'Not a known government domain (you might need to update domains.yml)' -@pytest.mark.parametrize('error_field', ['name', 'domain']) -def test_create_letter_branding_shows_database_errors_on_name_and_domain_fields( +def test_create_letter_branding_shows_database_errors_on_name_fields( mocker, logged_in_platform_admin_client, fake_uuid, - error_field ): with logged_in_platform_admin_client.session_transaction() as session: user_id = session["user_id"] @@ -576,13 +524,13 @@ def test_create_letter_branding_shows_database_errors_on_name_and_domain_fields( json={ 'result': 'error', 'message': { - error_field: { - '{} already in use'.format(error_field) + 'name': { + 'name already in use' } } } ), - message={error_field: ['{} already in use'.format(error_field)]} + message={'name': ['name already in use']} )) temp_logo = LETTER_TEMP_LOGO_LOCATION.format(user_id=user_id, unique_id=fake_uuid, filename='test.svg') @@ -591,7 +539,6 @@ def test_create_letter_branding_shows_database_errors_on_name_and_domain_fields( url_for('.create_letter_branding', logo=temp_logo), data={ 'name': 'my brand', - 'domain': None, 'operation': 'branding-details' } ) @@ -600,7 +547,7 @@ def test_create_letter_branding_shows_database_errors_on_name_and_domain_fields( error_message = page.find('span', class_='error-message').text.strip() assert page.find('h1').text == 'Add letter branding' - assert error_message == '{} already in use'.format(error_field) + assert error_message == 'name already in use' def test_get_png_file_from_svg(client, mocker, fake_uuid): diff --git a/tests/app/notify_client/test_email_branding_client.py b/tests/app/notify_client/test_email_branding_client.py index ef1b48587..3ea1aa293 100644 --- a/tests/app/notify_client/test_email_branding_client.py +++ b/tests/app/notify_client/test_email_branding_client.py @@ -53,13 +53,13 @@ def test_get_all_email_branding(mocker): def test_create_email_branding(mocker): org_data = {'logo': 'test.png', 'name': 'test name', 'text': 'test name', 'colour': 'red', - 'domain': 'sample.com', 'brand_type': 'org'} + 'brand_type': 'org'} mock_post = mocker.patch('app.notify_client.email_branding_client.EmailBrandingClient.post') mock_redis_delete = mocker.patch('app.extensions.RedisClient.delete') EmailBrandingClient().create_email_branding( logo=org_data['logo'], name=org_data['name'], text=org_data['text'], colour=org_data['colour'], - domain=org_data['domain'], brand_type='org' + brand_type='org' ) mock_post.assert_called_once_with( @@ -72,13 +72,13 @@ def test_create_email_branding(mocker): def test_update_email_branding(mocker, fake_uuid): org_data = {'logo': 'test.png', 'name': 'test name', 'text': 'test name', 'colour': 'red', - 'domain': 'sample.com', 'brand_type': 'org'} + 'brand_type': 'org'} mock_post = mocker.patch('app.notify_client.email_branding_client.EmailBrandingClient.post') mock_redis_delete = mocker.patch('app.extensions.RedisClient.delete') EmailBrandingClient().update_email_branding( branding_id=fake_uuid, logo=org_data['logo'], name=org_data['name'], text=org_data['text'], - colour=org_data['colour'], domain=org_data['domain'], brand_type='org') + colour=org_data['colour'], brand_type='org') mock_post.assert_called_once_with( url='/email-branding/{}'.format(fake_uuid), diff --git a/tests/app/notify_client/test_letter_branding_client.py b/tests/app/notify_client/test_letter_branding_client.py index a6e9db49d..3b2d74a32 100644 --- a/tests/app/notify_client/test_letter_branding_client.py +++ b/tests/app/notify_client/test_letter_branding_client.py @@ -39,13 +39,13 @@ def test_get_all_letter_branding(mocker): def test_create_letter_branding(mocker): - new_branding = {'filename': 'uuid-test', 'name': 'my letters', 'domain': 'example.com'} + new_branding = {'filename': 'uuid-test', 'name': 'my letters'} mock_post = mocker.patch('app.notify_client.letter_branding_client.LetterBrandingClient.post') mock_redis_delete = mocker.patch('app.extensions.RedisClient.delete') LetterBrandingClient().create_letter_branding( - filename=new_branding['filename'], name=new_branding['name'], domain=new_branding['domain'] + filename=new_branding['filename'], name=new_branding['name'], ) mock_post.assert_called_once_with( url='/letter-branding', @@ -56,12 +56,12 @@ def test_create_letter_branding(mocker): def test_update_letter_branding(mocker, fake_uuid): - branding = {'filename': 'uuid-test', 'name': 'my letters', 'domain': 'example.com'} + branding = {'filename': 'uuid-test', 'name': 'my letters'} mock_post = mocker.patch('app.notify_client.letter_branding_client.LetterBrandingClient.post') mock_redis_delete = mocker.patch('app.extensions.RedisClient.delete') LetterBrandingClient().update_letter_branding( - branding_id=fake_uuid, filename=branding['filename'], name=branding['name'], domain=branding['domain']) + branding_id=fake_uuid, filename=branding['filename'], name=branding['name']) mock_post.assert_called_once_with( url='/letter-branding/{}'.format(fake_uuid), diff --git a/tests/conftest.py b/tests/conftest.py index 21274f0ab..4abb3aaa5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2539,8 +2539,8 @@ def mock_get_all_email_branding(mocker): non_standard_values = [ {'idx': 1, 'colour': 'red'}, {'idx': 2, 'colour': 'orange'}, - {'idx': 3, 'text': None, 'domain': 'nhs.uk'}, - {'idx': 4, 'colour': 'blue', 'domain': 'voa.gov.uk'}, + {'idx': 3, 'text': None}, + {'idx': 4, 'colour': 'blue'}, ] shuffle = sort_key is None return create_email_brandings(5, non_standard_values=non_standard_values, shuffle=shuffle) @@ -2559,19 +2559,16 @@ def mock_get_all_letter_branding(mocker): 'id': str(UUID(int=0)), 'name': 'HM Government', 'filename': 'hm-government', - 'domain': None, }, { 'id': str(UUID(int=1)), 'name': 'Land Registry', 'filename': 'land-registry', - 'domain': 'landregistry.gov.uk', }, { 'id': str(UUID(int=2)), 'name': 'Animal and Plant Health Agency', 'filename': 'animal', - 'domain': None, } ] @@ -2587,7 +2584,6 @@ def mock_get_letter_branding_by_id(mocker): 'id': _id, 'name': 'HM Government', 'filename': 'hm-government', - 'domain': 'cabinet-office.gov.uk', } return mocker.patch( 'app.letter_branding_client.get_letter_branding', side_effect=_get_branding_by_id @@ -2611,7 +2607,6 @@ def create_email_branding(id, non_standard_values={}): 'text': 'Organisation text', 'id': id, 'colour': '#f00', - 'domain': 'sample.com', 'brand_type': 'org', } @@ -2674,7 +2669,7 @@ def mock_get_email_branding_without_brand_text(mocker, fake_uuid): @pytest.fixture(scope='function') def mock_create_email_branding(mocker): - def _create_email_branding(logo, name, text, colour, domain, brand_type): + def _create_email_branding(logo, name, text, colour, brand_type): return return mocker.patch( @@ -2684,7 +2679,7 @@ def mock_create_email_branding(mocker): @pytest.fixture(scope='function') def mock_update_email_branding(mocker): - def _update_email_branding(branding_id, logo, name, text, colour, domain, brand_type): + def _update_email_branding(branding_id, logo, name, text, colour, brand_type): return return mocker.patch( From 9d99c78c3adf7a69d76203c73fb919c1aad1a016 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 4 Apr 2019 11:13:41 +0100 Subject: [PATCH 14/31] Remove code that reads `domains.yml` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since we’ve removed calls to this code from the rest of the app, it can safely be removed now. --- app/utils.py | 197 +++------------------------------------- tests/app/test_utils.py | 158 +------------------------------- 2 files changed, 16 insertions(+), 339 deletions(-) diff --git a/app/utils.py b/app/utils.py index 2a70ab3fb..de4ac69d8 100644 --- a/app/utils.py +++ b/app/utils.py @@ -13,15 +13,7 @@ import ago import dateutil import pyexcel import yaml -from flask import ( - Markup, - abort, - current_app, - redirect, - request, - session, - url_for, -) +from flask import abort, current_app, redirect, request, session, url_for from flask_login import current_user from notifications_utils.field import Field from notifications_utils.formatters import make_quotes_smart @@ -409,143 +401,23 @@ def set_status_filters(filter_args): _dir_path = os.path.dirname(os.path.realpath(__file__)) -class AgreementInfo: +class NotGovernmentEmailDomain(Exception): + pass - with open('{}/domains.yml'.format(_dir_path)) as domains: - domains = yaml.safe_load(domains) - domain_names = sorted(domains.keys(), key=len, reverse=True) + +class GovernmentEmailDomain(): + + with open('{}/email_domains.yml'.format(_dir_path)) as email_domains: + domain_names = yaml.safe_load(email_domains) def __init__(self, email_address_or_domain): - - self._match = next(filter( - self.get_matching_function(email_address_or_domain), - self.domain_names, - ), None) - - self._domain = email_address_or_domain.split('@')[-1] - - ( - self.owner, - self.crown_status, - self.agreement_signed, - self.canonical_domain, - ) = self._get_info() - - @classmethod - def from_user(cls, user): - return cls(user.email_address if user.is_authenticated else '') - - @classmethod - def from_current_user(cls): - return cls.from_user(current_user) - - @property - def as_human_readable(self): - if self.canonical_domain and 'dwp' in self.canonical_domain: - return 'DWP - Requires OED approval' - if self.agreement_signed: - return 'Yes, on behalf of {}'.format(self.owner) - elif self.owner: - return '{} (organisation is {}, {})'.format( - { - False: 'No', - None: 'Can’t tell', - }.get(self.agreement_signed), - self.owner, - { - True: 'a crown body', - False: 'a non-crown body', - None: 'crown status unknown', - }.get(self.crown_status), - ) - else: - return 'Can’t tell (domain is {})'.format(self._domain) - - @property - def as_info_for_branding_request(self): - return self.owner or 'Can’t tell (domain is {})'.format(self._domain) - - @property - def as_jinja_template(self): - if self.crown_status is None: - return 'agreement-choose' - if self.agreement_signed: - return 'agreement-signed' - return 'agreement' - - def as_terms_of_use_paragraph(self, **kwargs): - return Markup(self._as_terms_of_use_paragraph(**kwargs)) - - def _as_terms_of_use_paragraph(self, terms_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.' - ).format(self._acceptance_required, terms_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(self._acceptance_required, download_link, support_link)) - - if self.agreement_signed is False: - return (( - '{} Download a copy.' - ).format(self._acceptance_required, download_link)) - - return ( - 'Your organisation ({}) has already accepted the ' - 'GOV.UK Notify data sharing and financial ' - 'agreement.'.format(self.owner) - ) - - 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.owner, - { - True: 'has already accepted it', - False: 'hasn’t accepted it yet' - }.get(self.agreement_signed) - ) - ) - - @property - def _acceptance_required(self): - return ( - 'Your organisation {} must also accept our data sharing ' - 'and financial agreement.'.format( - '({})'.format(self.owner) if self.owner else '', - ) - ) - - @property - def crown_status_or_404(self): - if self.crown_status is None: - abort(404) - return self.crown_status + try: + self._match = next(filter( + self.get_matching_function(email_address_or_domain), + self.domain_names, + )) + except StopIteration: + raise NotGovernmentEmailDomain() @staticmethod def get_matching_function(email_address_or_domain): @@ -564,45 +436,6 @@ class AgreementInfo: return fn - def _get_info(self): - - details = self.domains.get(self._match, {}) - - if details is None: - raise TypeError('Domain must have details ({})'.format(self._domain)) - - if isinstance(details, str): - self.is_canonical = False - return AgreementInfo(details)._get_info() - - elif isinstance(details, dict): - self.is_canonical = bool(details) - return( - details.get("owner"), - details.get("crown"), - details.get("agreement_signed"), - self._match, - ) - - -class NotGovernmentEmailDomain(Exception): - pass - - -class GovernmentEmailDomain(AgreementInfo): - - with open('{}/email_domains.yml'.format(_dir_path)) as email_domains: - domain_names = yaml.safe_load(email_domains) - - def __init__(self, email_address_or_domain): - try: - self._match = next(filter( - self.get_matching_function(email_address_or_domain), - self.domain_names, - )) - except StopIteration: - raise NotGovernmentEmailDomain() - def unicode_truncate(s, length): encoded = s.encode('utf-8')[:length] diff --git a/tests/app/test_utils.py b/tests/app/test_utils.py index c7fb087a8..cb5a85b1c 100644 --- a/tests/app/test_utils.py +++ b/tests/app/test_utils.py @@ -1,16 +1,13 @@ -from collections import Counter, OrderedDict +from collections import OrderedDict from csv import DictReader from io import StringIO from pathlib import Path import pytest from freezegun import freeze_time -from notifications_utils.recipients import validate_email_address from app import format_datetime_relative from app.utils import ( - AgreementInfo, - GovernmentEmailDomain, Spreadsheet, email_safe, generate_next_dict, @@ -292,159 +289,6 @@ def test_get_cdn_domain_on_non_localhost(client, mocker): assert domain == 'static-logos.admintest.com' -@pytest.mark.parametrize("domain_or_email_address", ( - "test@dclgdatamart.co.uk", "test@communities.gsi.gov.uk", "test@communities.gov.uk", -)) -def test_get_valid_agreement_info_known_details(domain_or_email_address): - agreement_info = AgreementInfo(domain_or_email_address) - assert agreement_info.crown_status is None - assert agreement_info.owner == "Ministry of Housing, Communities & Local Government" - assert agreement_info.agreement_signed is True - assert agreement_info.as_human_readable == ( - 'Yes, on behalf of Ministry of Housing, Communities & Local Government' - ) - - -@pytest.mark.parametrize("domain_or_email_address", ( - "test@dwp.gov.uk", "test@dwp.gsi.gov.uk", -)) -def test_dwp_go_live_requests_are_flagged(domain_or_email_address): - agreement_info = AgreementInfo(domain_or_email_address) - assert agreement_info.owner == "Department for Work and Pensions" - assert agreement_info.agreement_signed is True - assert agreement_info.as_human_readable == ( - 'DWP - Requires OED approval' - ) - - -@pytest.mark.parametrize("domain_or_email_address, is_canonical", ( - ("test@dclgdatamart.co.uk", False), - ("test@communities.gsi.gov.uk", False), - ("test@communities.gov.uk", True), -)) -def test_get_canonical_domain(domain_or_email_address, is_canonical): - assert AgreementInfo(domain_or_email_address).canonical_domain == 'communities.gov.uk' - assert AgreementInfo(domain_or_email_address).is_canonical == is_canonical - - -def test_get_canonical_domain_passes_through_unknown_domain(): - assert AgreementInfo('example.com').canonical_domain is None - assert AgreementInfo('example.com').is_canonical is False - - -@pytest.mark.parametrize("domain_or_email_address", ( - "test@police.gov.uk", "police.gov.uk", -)) -def test_get_valid_agreement_info_unknown_details(domain_or_email_address): - government_domain = AgreementInfo(domain_or_email_address) - assert government_domain.crown_status is None - assert government_domain.owner is None - assert government_domain.agreement_signed is None - assert government_domain.as_human_readable == 'Can’t tell (domain is police.gov.uk)' - - -def test_get_valid_agreement_info_only_org_known(): - agreement_info = AgreementInfo('nhs.net') - # Some parts of the NHS are Crown, some aren’t - assert agreement_info.crown_status is None - assert agreement_info.owner == 'NHS' - assert agreement_info.agreement_signed is None - assert agreement_info.as_human_readable == 'Can’t tell (organisation is NHS, crown status unknown)' - - -def test_get_valid_agreement_info_some_known_details(): - agreement_info = AgreementInfo("marinemanagement.org.uk") - assert agreement_info.crown_status is None - assert agreement_info.owner == "Marine Management Organisation" - assert agreement_info.agreement_signed is True - assert agreement_info.as_human_readable == ( - 'Yes, on behalf of Marine Management Organisation' - ) - - -def test_get_valid_local_agreement_info_some_known_details(): - # This example may need to be updated to use a different council if - # Babergh every sign the agreement - agreement_info = AgreementInfo("babergh.gov.uk") - assert agreement_info.crown_status is False - assert agreement_info.owner == "Babergh District Council" - assert agreement_info.agreement_signed is False - assert agreement_info.as_human_readable == ( - 'No (organisation is Babergh District Council, a non-crown body)' - ) - - -def test_get_valid_government_domain_gets_most_specific_first(): - - generic = AgreementInfo("gov.uk") - assert generic.crown_status is None - assert generic.owner is None - assert generic.agreement_signed is None - assert generic.as_human_readable == ( - 'Can’t tell (domain is gov.uk)' - ) - - specific = AgreementInfo("dacorum.gov.uk") - assert specific.crown_status is False - assert specific.owner == 'Dacorum Borough Council' - assert specific.agreement_signed is True - assert specific.as_human_readable == ( - 'Yes, on behalf of Dacorum Borough Council' - ) - - -def test_get_domain_info_for_branding_request(): - - assert AgreementInfo("gov.uk").as_info_for_branding_request == ( - 'Can’t tell (domain is gov.uk)' - ) - assert AgreementInfo("dacorum.gov.uk").as_info_for_branding_request == ( - 'Dacorum Borough Council' - ) - - -def test_domains_are_lowercased(): - for domain in AgreementInfo.domains.keys(): - assert domain == domain.lower() - - -def test_validate_government_domain_data(): - - for domain in AgreementInfo.domains.keys(): - - validate_email_address('test@{}'.format(domain)) - - agreement_info = AgreementInfo(domain) - - assert agreement_info.crown_status in { - True, False, None - } - - assert isinstance(agreement_info.owner, str) and agreement_info.owner.strip() - - assert agreement_info.agreement_signed in { - True, False, None - } - - -def test_domain_data_is_canonicalized(): - for owner, count in Counter( - AgreementInfo(domain).owner - for domain in AgreementInfo.domains.keys() - if AgreementInfo(domain).is_canonical - ).most_common(): - if count > 1: - raise ValueError( - '{} entries in domains.yml for {}'.format(count, owner) - ) - - -def test_validate_email_domain_data(): - - for domain in GovernmentEmailDomain.domains.keys(): - validate_email_address('test@{}'.format(domain)) - - @pytest.mark.parametrize('time, human_readable_datetime', [ ('2018-03-14 09:00', '14 March at 9:00am'), ('2018-03-14 15:00', '14 March at 3:00pm'), From 5d091b214a4d682ae14d084949e380de90786f1d Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 4 Apr 2019 11:13:53 +0100 Subject: [PATCH 15/31] Remove domains.yml file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since we’ve now removed the code that reads this file, it can safely be deleted. --- app/domains.yml | 4429 ----------------------------------------------- 1 file changed, 4429 deletions(-) delete mode 100644 app/domains.yml diff --git a/app/domains.yml b/app/domains.yml deleted file mode 100644 index 830d0bbc7..000000000 --- a/app/domains.yml +++ /dev/null @@ -1,4429 +0,0 @@ ---- -# Central and NHS -slc.co.uk: - owner: Student Loans Company -dclgdatamart.co.uk: communities.gov.uk -communities.gsi.gov.uk: communities.gov.uk -communities.gov.uk: - owner: "Ministry of Housing, Communities & Local Government" - agreement_signed: true -hmcts.net: hmcts.gov.uk -hmcts.gsi.gov.uk: hmcts.gov.uk -hmcts.gov.uk: - owner: Her Majesty’s Courts and Tribunals Service - agreement_signed: true -cqc.org.uk: - owner: Care Quality Commission - agreement_signed: true -marinemanagement.org.uk: - owner: Marine Management Organisation - agreement_signed: true -crowncommercial.gov.uk: cabinet-office.gov.uk -cabinetoffice.gov.uk: cabinet-office.gov.uk -cabinet-office.gov.uk: - owner: Cabinet Office - crown: true - agreement_signed: true -cica.gsi.gov.uk: cica.gov.uk -cica.gov.uk: - owner: Criminal Injuries Compensation Authority - agreement_signed: true -dbs.gsi.gov.uk: dbs.gov.uk -dbs.gov.uk: - owner: Disclosure and Barring Service - agreement_signed: true -dvla.gsi.gov.uk: dvla.gov.uk -dvla.gov.uk: - owner: Driver and Vehicle Licensing Agency - agreement_signed: true -environment-agency.gsi.gov.uk: environment-agency.gov.uk -environment-agency.gov.uk: - owner: Environment Agency - agreement_signed: true -food.gov.uk: foodstandards.gov.uk -foodstandards.gsi.gov.uk: foodstandards.gov.uk -foodstandards.gov.uk: - owner: Food Standards Agency - agreement_signed: true -gchq.gov.uk: - owner: Government Communications Headquarters - crown: true - agreement_signed: true -gla.gsi.gov.uk: gla.gov.uk -gla.gov.uk: - owner: The Gangmasters and Labour Abuse Authority - agreement_signed: true -governmentlegal.gov.uk: - owner: Government Legal Department - agreement_signed: true -gov.scot: - owner: Scottish Government - crown: true - agreement_signed: true -innovateuk.gsi.gov.uk: innovateuk.gov.uk -innovateuk.gov.uk: - owner: Innovate UK - agreement_signed: true -ncsc.gov.uk: gchq.gov.uk -nhs.net: nhs.uk -nhs.uk: - owner: NHS -nhsdigital.nhs.uk: digital.nhs.uk -digital.nhs.uk: - owner: NHS Digital - agreement_signed: true -ofgem.gov.uk: - owner: Ofgem - crown: true - agreement_signed: true -ofsted.gsi.gov.uk: ofsted.gov.uk -ofsted.gov.uk: - owner: Ofsted - agreement_signed: true -ons.gsi.gov.uk: ons.gov.uk -statistics.gov.uk: ons.gov.uk -ons.gov.uk: - owner: Office for National Statistics - agreement_signed: true -pins.gsi.gov.uk: pins.gov.uk -pins.gov.uk: - owner: The Planning Inspectorate - agreement_signed: true - crown: true -publicguardian.gsi.gov.uk: publicguardian.gov.uk -publicguardian.gov.uk: - owner: Office of the Public Guardian - agreement_signed: true -srft.nhs.uk: - owner: Salford Royal NHS Trust - agreement_signed: true -btuh.nhs.uk: - owner: Basildon and Thurrock University Hospitals NHS Foundation Trust - agreement_signed: true -nationalarchives.gsi.gov.uk: nationalarchives.gov.uk -nationalarchives.gov.uk: - owner: National Archives - agreement_signed: true -voa.gsi.gov.uk: voa.gov.uk -voa.gov.uk: - owner: Valuation Office Agency - agreement_signed: true -daera-ni.gov.uk: - owner: Department of Agriculture Environment and Rural Affairs - agreement_signed: true -beis.gsi.gov.uk: beis.gov.uk -beis.gov.uk: - owner: Department for Business, Energy and Industrial Strategy - agreement_signed: true -education.gsi.gov.uk: education.gov.uk -education.gov.uk: - owner: Department for Education - agreement_signed: true -gad.gsi.gov.uk: gad.gov.uk -gad.gov.uk: - owner: Government Actuary's Department - agreement_signed: true -landregistry.gsi.gov.uk: landregistry.gov.uk -landregistry.gov.uk: - owner: HM Land Registry - agreement_signed: true -legalaid.gsi.gov.uk: legalaid.gov.uk -legalaid.gov.uk: - owner: Legal Aid Agency - agreement_signed: true -phe.gsi.gov.uk: phe.gov.uk -phe.gov.uk: - owner: Public Health England - agreement_signed: true -ssro.gsi.gov.uk: ssro.gov.uk -ssro.gov.uk: - owner: Single Source Regulations Office - agreement_signed: true -vmd.defra.gsi.gov.uk: vmd.defra.gov.uk -vmd.defra.gov.uk: - owner: Vetinary Medicines Directorate - agreement_signed: true -companieshouse.gsi.gov.uk: companieshouse.gov.uk -companieshouse.gov.uk: - owner: Companies House - agreement_signed: true -trade.gsi.gov.uk: trade.gov.uk -trade.gov.uk: - owner: Department for International Trade - agreement_signed: true -ips.gsi.gov.uk: ips.gov.uk -ips.gov.uk: hmpo.gov.uk -hmpo.gov.uk: homeoffice.gov.uk -hmpo.gsi.gov.uk: homeoffice.gov.uk -homeoffice.gsi.gov.uk: homeoffice.gov.uk -homeoffice.gov.uk: - owner: Home Office - agreement_signed: true -ipo.gsi.gov.uk: ipo.gov.uk -ipo.gov.uk: - owner: Intellectual Property Office - agreement_signed: true -judicialappointments.digital: justice.gov.uk -judicialappointments.gov.uk: justice.gov.uk -justice.gsi.gov.uk: justice.gov.uk -justice.gov.uk: - owner: Ministry of Justice - agreement_signed: true -rpa.gsi.gov.uk: rpa.gov.uk -rpa.gov.uk: - owner: Rural Payments Agency - agreement_signed: true -mcga.gov.uk: - owner: Maritime and Coastguard Agency - agreement_signed: true -metoffice.gov.uk: - owner: Met Office - agreement_signed: false - crown: true -dft.gsi.gov.uk: dft.gov.uk -dft.gov.uk: - owner: Department for Transport - agreement_signed: true - crown: true -innovateuk.gov.uk: - owner: Innovate UK - agreement_signed: false - crown: false -gamblingcommission.gov.uk: - owner: Gambling Commission - agreement_signed: true - crown: false -geo.gov.uk: - owner: Government Equalities Office - agreement_signed: true - crown: true -insolvency.gsi.gov.uk: insolvency.gov.uk -insolvency.gov.uk: - owner: The Insolvency Service - agreement_signed: true - crown: true -dwp.gsi.gov.uk: dwp.gov.uk -dwp.gov.uk: - owner: Department for Work and Pensions - agreement_signed: true - crown: true -vca.gov.uk: - owner: Vehicle Certification Agency - agreement_signed: false - crown: true -vca.gsi.gov.uk: vca.gov.uk -olev.gov.uk: dft.gov.uk -olev.gsi.gov.uk: olev.gov.uk -apha.gsi.gov.uk: apha.gov.uk -apha.gov.uk: - owner: Animal & Plant Health Agency - crown: true - agreement_signed: true -mil.uk: mod.gov.uk -moduk.org: mod.gov.uk -dera.gov.uk: mod.gov.uk -dstl.gov.uk: mod.gov.uk -mod.gov.uk: - owner: Ministry of Defence - crown: true - agreement_signed: -defra.gsi.gov.uk: defra.gov.uk -defra.gov.uk: - owner: Department for Environment, Food & Rural Affairs - crown: true - agreement_signed: -hmrc.gsi.gov.uk: hmrc.gov.uk -hmrc.gov.uk: - owner: HM Revenue and Customs - crown: true - agreement_signed: - -# Crown bodies who haven’t signed the MOU -acas.org.uk: - owner: Advisory, Conciliation and Arbitration Service - crown: true - agreement_signed: false -charitycommission.gsi.gov.uk: charitycommission.gov.uk -charitycommission.gov.uk: - owner: Charity Commission - crown: true - agreement_signed: false -cps.gov.uk: - owner: The Crown Prosecution Service - crown: true - agreement_signed: false -culture.gov.uk: - owner: Department for Digital, Culture, Media & Sport - crown: true - agreement_signed: true -culture.gsi.gov.uk: culture.gov.uk -dexeu.gov.uk: - owner: Department for Exiting the European Union - crown: true - agreement_signed: false -dvsa.gov.uk: - owner: Driver & Vehicle Standards Agency - crown: true - agreement_signed: true -dfid.gov.uk: - owner: Department for International Development - crown: true - agreement_signed: false -fco.gov.uk: - owner: Foreign & Commonwealth Office - crown: true - agreement_signed: false -hse.gov.uk: - owner: Health and Safety Executive - crown: true - agreement_signed: false -hmtreasury.gsi.gov.uk: hmtreasury.gov.uk -hmtreasury.gov.uk: - owner: HM Treasury - crown: true - agreement_signed: false -nio.gov.uk: - owner: Northern Ireland Office - crown: true - agreement_signed: false -ukexportfinance.gov.uk: - owner: UK Export Finance - crown: true - agreement_signed: false -ppf.gsi.gov.uk: ppf.gov.uk -ppf.gov.uk: pensionprotectionfund.org.uk -pensionprotectionfund.org.uk: - owner: Pension Protection Fund - crown: true - agreement_signed: true -naturalresources.wales: cyfoethnaturiolcymru.gov.uk -cyfoethnaturiolcymru.gov.uk: - owner: Natural Resources Wales - crown: true - agreement_signed: true -gov.wales: - owner: Welsh Government - crown: true - agreement_signed: false - -# Other non-crown -bl.uk: - owner: British Library - crown: false - agreement_signed: false -nao.org.uk: - owner: National Audit Office - crown: false - agreement_signed: false - -# Local Government -aberdeencityandshire-sdpa.gov.uk: aberdeencity.gov.uk -aberdeencity.gov.uk: - owner: Aberdeen City Council - crown: false - agreement_signed: false -aberdeenshire.gov.uk: - owner: Aberdeenshire Council - crown: false - agreement_signed: true -abergavennytowncouncil.gov.uk: - owner: Abergavenny Town Council - crown: false - agreement_signed: false -aberporthcommunitycouncil.gov.uk: - owner: Aberporth Community Council - crown: false - agreement_signed: false -abertilleryandllanhilleth-wcc.gov.uk: - owner: Abertilly and Llanhilleth Community Council - crown: false - agreement_signed: false -aberystwyth.gov.uk: - owner: Aberystwyth Town Council - crown: false - agreement_signed: false -abingdon.gov.uk: - owner: Abingdon Town Council - crown: false - agreement_signed: false -abinger-pc.gov.uk: molevalley.gov.uk -actoncommunitycouncil.gov.uk: - owner: Acton Community Council - crown: false - agreement_signed: false -adurdc.gov.uk: adur.gov.uk -adur.gov.uk: - owner: Adur District Council - crown: false - agreement_signed: false -adur-worthing.gov.uk: adur.gov.uk -alcester-tc.gov.uk: - owner: Alcester Town Council - crown: false - agreement_signed: false -allerdale.gov.uk: - owner: Allerdale Borough Council - crown: false - agreement_signed: false -alnwick-tc.gov.uk: - owner: Alnwick Town Council - crown: false - agreement_signed: false -alton.gov.uk: - owner: Alton Town Council - crown: false - agreement_signed: false -ambervalley.gov.uk: - owner: Amber Valley Borough Council - crown: false - agreement_signed: false -amble.gov.uk: - owner: Amble Town Council - crown: false - agreement_signed: false -amersham-tc.gov.uk: - owner: Amersham Town Council - crown: false - agreement_signed: false -amesburytowncouncil.gov.uk: - owner: Amesbury Town Council - crown: false - agreement_signed: false -andover-tc.gov.uk: - owner: Andover Town Council - crown: false - agreement_signed: false -anglesey.gov.uk: - owner: Isle of Anglesey County Council - crown: false - agreement_signed: false -angliarevenues.gov.uk: westsuffolk.gov.uk -angus.gov.uk: - owner: Angus Council - crown: false - agreement_signed: true -antrimandnewtownabbey.gov.uk: antrim.gov.uk -antrim.gov.uk: - owner: Antrim Borough Council - crown: false - agreement_signed: true -archifaugwent.gov.uk: blaenau-gwent.gov.uk -archifaumorgannwg.gov.uk: cardiff.gov.uk -ardsandnorthdown.gov.uk: - owner: North Down Borough Council - crown: false - agreement_signed: false -argoedcommunitycouncil.gov.uk: - owner: Argoed Community Council - crown: false - agreement_signed: false -argyll-bute.gov.uk: - owner: Argyll and Bute Council - crown: false - agreement_signed: false -armaghbanbridgecraigavon.gov.uk: - owner: Armagh City and District Council - crown: false - agreement_signed: false -arundeltowncouncil.gov.uk: - owner: Arundel Town Council - crown: false - agreement_signed: false -arun.gov.uk: - owner: Arun District Council - crown: false - agreement_signed: false -ashbournetowncouncil.gov.uk: - owner: Ashbourne Town Council - crown: false - agreement_signed: false -ashfield-dc.gov.uk: ashfield.gov.uk -ashfield.gov.uk: - owner: Ashfield District Council - crown: false - agreement_signed: true -ashford.gov.uk: - owner: Ashford Borough Council - crown: false - agreement_signed: false -ashingtontowncouncil.gov.uk: - owner: Ashington Town Council - crown: false - agreement_signed: false -ashurstwood-vc.gov.uk: - owner: Ashurst Wood Village Council - crown: false - agreement_signed: false -askderbyshire.gov.uk: ne-derbyshire.gov.uk -aspatria-tc.gov.uk: - owner: Aspatria Town Council - crown: false - agreement_signed: false -atherstone-tc.gov.uk: - owner: Atherstone Town Council - crown: false - agreement_signed: false -aws.gov.uk: worthing.gov.uk -axbridge-tc.gov.uk: - owner: Axbridge Town Council - crown: false - agreement_signed: false -axminstertowncouncil.gov.uk: - owner: Axminster Town Council - crown: false - agreement_signed: false -aylesbury.gov.uk: aylesburyvale.gov.uk -aylesburytowncouncil.gov.uk: - owner: Aylesbury Town Council - crown: false - agreement_signed: false -aylesburyvaledc.gov.uk: aylesburyvale.gov.uk -aylesburyvale.gov.uk: - owner: Aylesbury Vale District Council - crown: false - agreement_signed: false -aylsham-tc.gov.uk: - owner: Aylsham Town Council - crown: false - agreement_signed: false -ayrshire.gov.uk: south-ayrshire.gov.uk -ayrshire-jsu.gov.uk: south-ayrshire.gov.uk -babergh.gov.uk: - owner: Babergh District Council - crown: false - agreement_signed: false -baberghmidsuffolk.gov.uk: babergh.gov.uk -bagilltcommunitycouncil.gov.uk: - owner: Bagillt Community Council - crown: false - agreement_signed: false -baildontowncouncil.gov.uk: - owner: Baildon Town Council - crown: false - agreement_signed: false -bakewelltowncouncil.gov.uk: - owner: Bakewell Town Council - crown: false - agreement_signed: false -ballymoney.gov.uk: - owner: Ballymoney Borough Council - crown: false - agreement_signed: false -bamptontowncouncil.gov.uk: - owner: BamptonTown Council - crown: false - agreement_signed: false -banbury.gov.uk: - owner: Banbury Town Council - crown: false - agreement_signed: false -barking-dagenham.gov.uk: - owner: Barking & Dagenham London Borough Council - crown: false - agreement_signed: false -barmouthtowncouncil.gov.uk: - owner: Barmouth Town Council - crown: false - agreement_signed: false -barnardcastletowncouncil.gov.uk: - owner: Barnard Castle Town Council - crown: false - agreement_signed: false -barnet.gov.uk: - owner: Barnet London Borough Council - crown: false - agreement_signed: false -barnoldswicktowncouncil.gov.uk: - owner: Barnoldswick Town Council - crown: false - agreement_signed: false -barnsley.gov.uk: - owner: Barnsley Metropolitan Borough Council - crown: false - agreement_signed: true -barrowbc.gov.uk: - owner: Barrow in Furness Borough Council - crown: false - agreement_signed: false -barrytowncouncil.gov.uk: valeofglamorgan.gov.uk -basildon.gov.uk: - owner: Basildon District Council - crown: false - agreement_signed: true -basingstoke.gov.uk: - owner: Basingstoke and Deane Borough Council - crown: false - agreement_signed: false -bassetlaw.gov.uk: - owner: Bassetlaw District Council - crown: false - agreement_signed: false -batchworth-ecc.gov.uk: batchworth-pc.gov.uk -batchworth-pc.gov.uk: - owner: Batchworth Community Council - crown: false - agreement_signed: false -bathnes.gov.uk: - owner: Bath and North East Somerset Council - crown: false - agreement_signed: true -battletowncouncil.gov.uk: - owner: Battle Town Council - crown: false - agreement_signed: false -beaconsfieldtowncouncil.gov.uk: - owner: Beaconsfield Town Council - crown: false - agreement_signed: false -beaminster-tc.gov.uk: - owner: Beaminster Town Council - crown: false - agreement_signed: false -beaumaristowncouncil.gov.uk: - owner: Beaumaris Town Council - crown: false - agreement_signed: false -bedale-tc.gov.uk: - owner: Bedale Town Council - crown: false - agreement_signed: false -bedford.gov.uk: - owner: Bedford Borough Council - crown: false - agreement_signed: false -bedscc.gov.uk: bedford.gov.uk -bedsparishes.gov.uk: bedford.gov.uk -belfastcity.gov.uk: - owner: Belfast City Council - crown: false - agreement_signed: false -belpertowncouncil.gov.uk: - owner: Belper Town Council - crown: false - agreement_signed: false -berkeley-tc.gov.uk: - owner: Berkeley Town Council - crown: false - agreement_signed: false -berkhamstedtowncouncil.gov.uk: - owner: Berkhamsted Town Council - crown: false - agreement_signed: false -berwick-tc.gov.uk: - owner: Berwick-upon-Tweed Town Council - crown: false - agreement_signed: false -betchworth-pc.gov.uk: molevalley.gov.uk -beverley.gov.uk: - owner: Beverley Town Council - crown: false - agreement_signed: false -bexley.gov.uk: - owner: Bexley London Borough Council - crown: false - agreement_signed: false -bicester.gov.uk: - owner: Bicester Town Council - crown: false - agreement_signed: false -bideford-tc.gov.uk: - owner: Bideford Town Council - crown: false - agreement_signed: false -bidfordonavon-pc.gov.uk: stratford.gov.uk -biggleswadetowncouncil.gov.uk: - owner: Biggleswade Town Council - crown: false - agreement_signed: false -billericaytowncouncil.gov.uk: - owner: Billericay Town Council - crown: false - agreement_signed: false -bingham-tc.gov.uk: - owner: Bingham Town Council - crown: false - agreement_signed: false -bingleytowncouncil.gov.uk: - owner: Bingley Town Council - crown: false - agreement_signed: false -birmingham.gov.uk: - owner: Birmingham City Council - crown: false - agreement_signed: true -birminghamprepared.gov.uk: birmingham.gov.uk -bishopauckland-tc.gov.uk: - owner: Bishop Auckland Town Council - crown: false - agreement_signed: false -bishopsitchington-pc.gov.uk: stratford.gov.uk -bishopsstortfordtc.gov.uk: - owner: Bishop's Stortford Town Council - crown: false - agreement_signed: false -blaby.gov.uk: - owner: Blaby District Council - crown: false - agreement_signed: false -blackburn.gov.uk: - owner: Blackburn with Darwen Borough Council - crown: false - agreement_signed: false -blackpool.gov.uk: - owner: Blackpool Council - crown: false - agreement_signed: true -blackwaterandhawleytowncouncil.gov.uk: - owner: Blackwater and Hawley Town Council - crown: false - agreement_signed: false -blaenau-gwent.gov.uk: - owner: Blaenau Gwent County Borough Council - crown: false - agreement_signed: true -blaenhonddan-wcc.gov.uk: - owner: Blaenhonddan Community Council - crown: false - agreement_signed: false -blandfordforum-tc.gov.uk: - owner: Blandford Forum Town Council - crown: false - agreement_signed: false -bletchleyfennystratford-tc.gov.uk: - owner: Bletchley and Fenny Stratford Town Council - crown: false - agreement_signed: false -bodfaricommunitycouncil.gov.uk: - owner: Bodfari Community Council - crown: false - agreement_signed: false -bodmin.gov.uk: - owner: Bodmin Town Council - crown: false - agreement_signed: false -bognorregis.gov.uk: - owner: Bognor Regis Town Council - crown: false - agreement_signed: false -bollington-tc.gov.uk: - owner: Bollington Town Council - crown: false - agreement_signed: false -bolsover.gov.uk: - owner: Bolsover District Council - crown: false - agreement_signed: false -bolton.gov.uk: - owner: Bolton Metropolitan Borough Council - crown: false - agreement_signed: false -boston.gov.uk: - owner: Boston Borough Council - crown: false - agreement_signed: false -bournemouth.gov.uk: - owner: Bournemouth Borough Council - crown: false - agreement_signed: true -boveytracey.gov.uk: - owner: Bovey Tracey Town Council - crown: false - agreement_signed: false -bracklacommunitycouncil.gov.uk: - owner: Brackla Community Council - crown: false - agreement_signed: false -brackleynorthants-tc.gov.uk: - owner: Brackley Town Council - crown: false - agreement_signed: false -bracknell-forest.gov.uk: bracknell.gov.uk -bracknellforest.gov.uk: bracknell.gov.uk -bracknell.gov.uk: - owner: Bracknell Forest Borough Council - crown: false - agreement_signed: true -bracknelltowncouncil.gov.uk: - owner: Bracknell Town Council - crown: false - agreement_signed: false -bradford.gov.uk: - owner: City of Bradford Metropolitan District Council - crown: false - agreement_signed: true -bradfordonavontowncouncil.gov.uk: - owner: Bradford on Avon Town Council - crown: false - agreement_signed: false -brading.gov.uk: - owner: Brading Town Council - crown: false - agreement_signed: false -bradleystoke.gov.uk: - owner: Bradley Stoke Town Council - crown: false - agreement_signed: false -bradninch-tc.gov.uk: - owner: Bradninch Town Council - crown: false - agreement_signed: false -braintree.gov.uk: - owner: Braintree District Council - crown: false - agreement_signed: false -bramshaw.gov.uk: newforest.gov.uk -breamore.gov.uk: newforest.gov.uk -breckland.gov.uk: - owner: Breckland District Council - crown: false - agreement_signed: false -breckland-sholland.gov.uk: breckland.gov.uk -brent.gov.uk: - owner: Brent London Borough Council - crown: false - agreement_signed: false -brentwood.gov.uk: - owner: Brentwood Borough Council - crown: false - agreement_signed: false -bridgend.gov.uk: - owner: Bridgend County Borough Council - crown: false - agreement_signed: false -bridgendtowncouncil.gov.uk: - owner: Bridgend Town Council - crown: false - agreement_signed: false -bridgnorthtowncouncil.gov.uk: - owner: Bridgnorth Town Council - crown: false - agreement_signed: false -bridgwatertowncouncil.gov.uk: - owner: Bridgwater Town Council - crown: false - agreement_signed: false -bridlington.gov.uk: - owner: Bridlington Town Council - crown: false - agreement_signed: false -bridport-tc.gov.uk: - owner: Bridport Town Council - crown: false - agreement_signed: false -brierfield.gov.uk: - owner: Brierfield Town Council - crown: false - agreement_signed: false -brighton-hove.gov.uk: - owner: Brighton & Hove City Council - crown: false - agreement_signed: true -bristol-city.gov.uk: bristol.gov.uk -bristol.gov.uk: - owner: Bristol City Council - crown: false - agreement_signed: false -brixhamtowncouncil.gov.uk: - owner: Brixham Town Council - crown: false - agreement_signed: false -broadland.gov.uk: - owner: Broadland District Council - crown: false - agreement_signed: false -broadstairs.gov.uk: - owner: Broadstairs and St Peters Town Council - crown: false - agreement_signed: false -brockenhurst.gov.uk: newforest.gov.uk -bromorgannwg.gov.uk: valeofglamorgan.gov.uk -bromsgroveandredditch.gov.uk: bromsgrove.gov.uk -bromsgrove.gov.uk: - owner: Bromsgrove District Council - crown: false - agreement_signed: false -bromyardandwinslow-tc.gov.uk: - owner: Bromley and Winslow Town Council - crown: false - agreement_signed: false -broughtonandbrettoncommunitycouncil.gov.uk: - owner: Broughton and Bretton Community Council - crown: false - agreement_signed: false -broughtonwrexhamcommunitycouncil.gov.uk: - owner: Broughton Community Council - crown: false - agreement_signed: false -broxbourne.gov.uk: - owner: Broxbourne Borough Council - crown: false - agreement_signed: false -broxtowe.gov.uk: - owner: Broxtowe Borough Council - crown: false - agreement_signed: false -brutontowncouncil.gov.uk: - owner: Bruton Town Council - crown: false - agreement_signed: false -brynfordcommunitycouncil.gov.uk: - owner: Brynford Community Council - crown: false - agreement_signed: false -buckfastleigh.gov.uk: - owner: Buckfastleigh Town Council - crown: false - agreement_signed: false -buckingham-tc.gov.uk: - owner: Buckingham Town Council - crown: false - agreement_signed: false -bucksandsurreytradingstandards.gov.uk: surreycc.gov.uk -buckinghamshirepartnership.gov.uk: buckscc.gov.uk -bucksalc.gov.uk: buckscc.gov.uk -buckscc.gov.uk: - owner: Buckinghamshire County Council - crown: false - agreement_signed: true -buckshomechoice.gov.uk: aylesburyvale.gov.uk -bude-stratton.gov.uk: - owner: Bude Stratton Town Council - crown: false - agreement_signed: false -budleighsaltertontowncouncil.gov.uk: - owner: Budleigh Salterton Town Council - crown: false - agreement_signed: false -buildingcontrolpartnershiphants.gov.uk: fareham.gov.uk -bungaytowncouncil.gov.uk: - owner: Bungay Town Council - crown: false - agreement_signed: false -buntingford-tc.gov.uk: - owner: Buntingford Town Council - crown: false - agreement_signed: false -burgesshill.gov.uk: - owner: Burgess Hill Town Council - crown: false - agreement_signed: false -burleyparishcouncil.gov.uk: newforest.gov.uk -burnhamoncrouchtowncouncil.gov.uk: - owner: Burnham-On-Crouch Town Council - crown: false - agreement_signed: false -burnley.gov.uk: - owner: Burnley Borough Council - crown: false - agreement_signed: false -burntwood-tc.gov.uk: - owner: Burntwood Town Council - crown: false - agreement_signed: false -bury.gov.uk: - owner: Bury Metropolitan Borough Council - crown: false - agreement_signed: false -burystedmunds-tc.gov.uk: westsuffolk.gov.uk -caerdydd.gov.uk: cardiff.gov.uk -caerffili.gov.uk: caerphilly.gov.uk -caernarfontowncouncil.gov.uk: - owner: Caernarfon Royal Town Council - crown: false - agreement_signed: false -caerphilly.gov.uk: - owner: Caerphilly County Borough Council - crown: false - agreement_signed: false -caiapark.gov.uk: - owner: Caia Park Community Council - crown: false - agreement_signed: false -cainscross-pc.gov.uk: stroud.gov.uk -calderdale.gov.uk: - owner: Calderdale Metropolitan Borough Council - crown: false - agreement_signed: true -callington-tc.gov.uk: - owner: Callington Town Council - crown: false - agreement_signed: false -calne.gov.uk: - owner: Calne Town Council - crown: false - agreement_signed: false -camborne-tc.gov.uk: - owner: Camborne Town Council - crown: false - agreement_signed: false -cambridge.gov.uk: - owner: Cambridge City Council - crown: false - agreement_signed: false -cambridgeshire.gov.uk: - owner: Cambridgeshire County Council - crown: false - agreement_signed: true -camden.gov.uk: - owner: Camden Council - crown: false - agreement_signed: true -camelford-tc.gov.uk: - owner: Camelford Town Council - crown: false - agreement_signed: false -cannockchasedc.gov.uk: - owner: Cannock Chase District Council - crown: false - agreement_signed: false -canterbury.gov.uk: - owner: Canterbury City Council - crown: false - agreement_signed: false -canveyisland-tc.gov.uk: - owner: Canvey Island Town Council - crown: false - agreement_signed: false -capel-pc.gov.uk: molevalley.gov.uk -cardiff.gov.uk: - owner: Cardiff Council - crown: false - agreement_signed: false -carlisle.gov.uk: - owner: Carlisle City Council - crown: false - agreement_signed: false -carltoncolvilletowncouncil.gov.uk: - owner: Carlton Colville Town Council - crown: false - agreement_signed: false -carmarthenshire.gov.uk: carmarthenshire.gov.wales -carmarthenshire.gov.wales: - owner: Carmarthenshire County Council - crown: false - agreement_signed: true -carmarthentowncouncil.gov.uk: carmarthenshire.gov.wales -carterton-tc.gov.uk: - owner: Carterton Town Council - crown: false - agreement_signed: false -castlepoint.gov.uk: - owner: Castle Point Borough Council - crown: false - agreement_signed: false -causewaycoastandglens.gov.uk: colerainebc.gov.uk -centralbedfordshire.gov.uk: - owner: Central Bedfordshire Council - crown: false - agreement_signed: false -ceredigion.llyw.cymru: ceredigion.gov.uk -ceredigion.gov.uk: - owner: Ceredigion County Council - crown: false - agreement_signed: false -chalford-pc.gov.uk: stroud.gov.uk -chard.gov.uk: - owner: Chard Town Council - crown: false - agreement_signed: false -charnwood.gov.uk: - owner: Charnwood Borough Council - crown: false - agreement_signed: false -chelmsford.gov.uk: - owner: Chelmsford City Council - crown: false - agreement_signed: false -chelmsleywood-tc.gov.uk: - owner: Chelmsley Wood Town Council - crown: false - agreement_signed: false -cheltenham.gov.uk: - owner: Cheltenham Borough Council - crown: false - agreement_signed: false -cherwellandsouthnorthants.gov.uk: southnorthants.gov.uk -cherwell-dc.gov.uk: cherwell.gov.uk -cherwell.gov.uk: - owner: Cherwell District Council - crown: false - agreement_signed: false -chesham.gov.uk: - owner: Chesham Town Council - crown: false - agreement_signed: false -cheshireeast.gov.uk: - owner: Cheshire East Council - crown: false - agreement_signed: true -cheshire.gov.uk: - owner: Cheshire West and Chester Council - crown: false - agreement_signed: true -cheshireprepared.gov.uk: cheshire.gov.uk -cheshiresharedservices.gov.uk: - owner: Crewe & Nantwich Borough Council - crown: false - agreement_signed: false -cheshirewestandchester.gov.uk: cheshire.gov.uk -cheshirewest.gov.uk: cheshire.gov.uk -chesterfield.gov.uk: - owner: Chesterfield Borough Council - crown: false - agreement_signed: false -chester.gov.uk: cheshire.gov.uk -chichestercity.gov.uk: - owner: Chichester City Council - crown: false - agreement_signed: false -chichester.gov.uk: - owner: Chichester District Council - crown: false - agreement_signed: false -chickerell-tc.gov.uk: - owner: Chickerell Town Council - crown: false - agreement_signed: false -chiltern.gov.uk: - owner: Chiltern District Council - crown: false - agreement_signed: true -chiltondurham-tc.gov.uk: - owner: Chilton Town Council - crown: false - agreement_signed: false -chippenham.gov.uk: - owner: Chippenham Town Council - crown: false - agreement_signed: false -chorley.gov.uk: - owner: Chorley Borough Council - crown: false - agreement_signed: false -christchurchandeastdorset.gov.uk: christchurch.gov.uk -christchurch.gov.uk: - owner: Christchurch Borough Council - crown: false - agreement_signed: false -chudleigh-tc.gov.uk: - owner: Chudleigh Town Council - crown: false - agreement_signed: false -churchaston-pc.gov.uk: telford.gov.uk -churchstretton-tc.gov.uk: - owner: Church Stretton Town Council - crown: false - agreement_signed: false -cilcaincommunitycouncil.gov.uk: - owner: Cilcain Community Council - crown: false - agreement_signed: false -cinderfordtowncouncil.gov.uk: - owner: Cinderford Town Council - crown: false - agreement_signed: false -cirencester.gov.uk: - owner: Cirencester Town Council - crown: false - agreement_signed: false -cityofworcester.gov.uk: worcester.gov.uk -clacks.gov.uk: - owner: Clackmannanshire Council - crown: false - agreement_signed: true -clevedon.gov.uk: - owner: Clevedon Town Council - crown: false - agreement_signed: false -clitheroetowncouncil.gov.uk: - owner: Clitheroe Town Council - crown: false - agreement_signed: false -cmktowncouncil.gov.uk: - owner: Central Milton Keynes Town Council - crown: false - agreement_signed: false -cncbuildingcontrol.gov.uk: south-norfolk.gov.uk -coastalwight.gov.uk: iow.gov.uk -coedffranc-wcc.gov.uk: - owner: Coedffranc Community Council - crown: false - agreement_signed: false -colchester.gov.uk: - owner: Colchester Borough Council - crown: false - agreement_signed: false -colefordtowncouncil.gov.uk: - owner: Coleford Town Council - crown: false - agreement_signed: false -colerainebc.gov.uk: - owner: Coleraine Borough Council - crown: false - agreement_signed: false -coleshilltowncouncil.gov.uk: - owner: Coleshill Town Council - crown: false - agreement_signed: false -colwyn-tc.gov.uk: - owner: Bay of Colwyn Town Council - crown: false - agreement_signed: false -cne-siar.gov.uk: - owner: Comhairle nan Eilean Siar - crown: false - agreement_signed: true -congleton-tc.gov.uk: - owner: Congleton Town Council - crown: false - agreement_signed: false -conwy.gov.uk: - owner: Conwy County Borough Council - crown: false - agreement_signed: true -conwytowncouncil.gov.uk: - owner: Conwy Town Council - crown: false - agreement_signed: false -cookstown.gov.uk: - owner: Cookstown District Council - crown: false - agreement_signed: false -copelandbc.gov.uk: copeland.gov.uk -copeland.gov.uk: - owner: Copeland Borough Council - crown: false - agreement_signed: false -corby.gov.uk: - owner: Corby Borough Council - crown: false - agreement_signed: true -cornwall-aonb.gov.uk: cornwall.gov.uk -cornwall.gov.uk: - owner: Cornwall Council - crown: false - agreement_signed: true -corsham.gov.uk: - owner: Corsham Town Council - crown: false - agreement_signed: false -cotgrave-tc.gov.uk: - owner: Cotgrave Town Council - crown: false - agreement_signed: false -cotswold.gov.uk: - owner: Cotswold District Council - crown: false - agreement_signed: false -coventry.gov.uk: - owner: Coventry City Council - crown: false - agreement_signed: false -cowbridge-tc.gov.uk: - owner: Cowbridge (Ancient Borough) and Llanblethian Town Council - crown: false - agreement_signed: false -cramlingtontowncouncil.gov.uk: - owner: Cramlington Town Council - crown: false - agreement_signed: false -cranbrooktowncouncil.gov.uk: - owner: Cranbrook Town Council - crown: false - agreement_signed: false -cravendc.gov.uk: - owner: Craven District Council - crown: false - agreement_signed: false -crawley.gov.uk: - owner: Crawley Borough Council - crown: false - agreement_signed: false -crediton.gov.uk: - owner: Credition Town Council - crown: false - agreement_signed: false -crewetowncouncil.gov.uk: - owner: Crewe Town Council - crown: false - agreement_signed: false -crewkerne-tc.gov.uk: - owner: Crewkerne Town Council - crown: false - agreement_signed: false -cricklade-tc.gov.uk: crickladetowncouncil.gov.uk -crickladetowncouncil.gov.uk: - owner: Cricklade Town Council - crown: false - agreement_signed: false -cringlefordparishcouncil.gov.uk: - owner: Cringleford Parich Council - crown: false - agreement_signed: false -cromer-tc.gov.uk: - owner: Cromer Town Council - crown: false - agreement_signed: false -crowboroughtowncouncil.gov.uk: - owner: Crowborough Town Council - crown: false - agreement_signed: false -croydon.gov.uk: - owner: Croydon Council - crown: false - agreement_signed: true -cullomptontowncouncil.gov.uk: - owner: Cullompton Town Council - crown: false - agreement_signed: false -culturalpropertyadvice.gov.uk: artscouncil.org.uk -cumbriacc.gov.uk: cumbria.gov.uk -cumbriafire.gov.uk: cumbria.gov.uk -cumbria.gov.uk: - owner: Cumbria County Council - crown: false - agreement_signed: false -currie-scc.gov.uk: - owner: Currie Community Council - crown: false - agreement_signed: false -cwmbran.gov.uk: torfaen.gov.uk -cyngortrefcaerfyrddin.gov.uk: carmarthenshire.gov.wales -cyngortrefrhuthun.gov.uk: ruthintowncouncil.gov.uk -cyngortrefybarri.gov.uk: valeofglamorgan.gov.uk -dab-vjb.gov.uk: west-dunbarton.gov.uk -dacorum.gov.uk: - owner: Dacorum Borough Council - crown: false - agreement_signed: true -darleydale.gov.uk: - owner: Darley Dale Town Council - crown: false - agreement_signed: false -darlington.gov.uk: - owner: Darlington Borough Council - crown: false - agreement_signed: false -dartford.gov.uk: - owner: Dartford Borough Council - crown: false - agreement_signed: false -daventrydc.gov.uk: - owner: Daventry District Council - crown: false - agreement_signed: false -daventrytowncouncil.gov.uk: - owner: Daventry Town Council - crown: false - agreement_signed: false -dawlish.gov.uk: - owner: Dawlish Town Council - crown: false - agreement_signed: false -deal.gov.uk: - owner: Deal Town Council - crown: false - agreement_signed: false -denbighshire.gov.uk: - owner: Denbighshire County Council - crown: false - agreement_signed: false -denbightowncouncil.gov.uk: denbighshire.gov.uk -derby.gov.uk: - owner: Derby City Council - crown: false - agreement_signed: false -derbyshirealc.gov.uk: - owner: Derbyshire Association of Local Councils - crown: false - agreement_signed: false -derbyshiredales.gov.uk: - owner: Derbyshire Dales District Council - crown: false - agreement_signed: false -derbyshire.gov.uk: - owner: Derbyshire County Council - crown: false - agreement_signed: false -derbyshirepartnership.gov.uk: derbyshire.gov.uk -desboroughtowncouncil.gov.uk: - owner: Desborough Town Council - crown: false - agreement_signed: false -devizes-tc.gov.uk: - owner: Devizes Town Council - crown: false - agreement_signed: false -devonaudit.gov.uk: devon.gov.uk -devonebiz.gov.uk: devon.gov.uk -devon.gov.uk: - owner: Devon County Council - crown: false - agreement_signed: true -devonjobs.gov.uk: devon.gov.uk -didcot.gov.uk: - owner: Didcot Town Council - crown: false - agreement_signed: false -diss.gov.uk: - owner: Diss Town Council - crown: false - agreement_signed: false -doncaster.gov.uk: - owner: Doncaster Metropolitan Borough Council - crown: false - agreement_signed: true -dorchester-tc.gov.uk: - owner: Dorchester Town Council - crown: false - agreement_signed: false -dorsetcouncil.gov.uk: dorsetcc.gov.uk -dorsetcc.gov.uk: - owner: Dorset County Council - crown: false - agreement_signed: true -dorsetforyou.gov.uk: weymouth.gov.uk -dorset.gov.uk: - owner: West Dorset District Council - crown: false - agreement_signed: false -dorsetwastepartnership.gov.uk: dorsetcc.gov.uk -dorset.gov.uk: dorsetcc.gov.uk -dover.gov.uk: - owner: Dover District Council - crown: false - agreement_signed: false -dovertowncouncil.gov.uk: - owner: Dover Town Council - crown: false - agreement_signed: false -downdc.gov.uk: - owner: Down District Council - crown: false - agreement_signed: false -droitwichspa.gov.uk: - owner: Droitwich Spa Town Council - crown: false - agreement_signed: false -dronfield.gov.uk: - owner: Dronfield Town Council - crown: false - agreement_signed: false -dudley.gov.uk: - owner: Dudley Metropolitan Borough Council - crown: false - agreement_signed: true -dumgal.gov.uk: - owner: Dumfries and Galloway Council - crown: false - agreement_signed: false -dundeecity.gov.uk: - owner: Dundee City Council - crown: false - agreement_signed: false -dungannon.gov.uk: - owner: Dungannon and South Tyrone Borough Council - crown: false - agreement_signed: false -dunstable.gov.uk: - owner: Dunstable Town Council - crown: false - agreement_signed: false -durhamcity.gov.uk: durham.gov.uk -durham.gov.uk: - owner: Durham County Council - crown: false - agreement_signed: false -durringtontowncouncil.gov.uk: - owner: Durrington Town Council - crown: false - agreement_signed: false -dursleytowncouncil.gov.uk: - owner: Dursley Town Council - crown: false - agreement_signed: false -ealing.gov.uk: - owner: Ealing Council - crown: false - agreement_signed: true -earley-tc.gov.uk: - owner: Earley Town Council - crown: false - agreement_signed: false -easingwold.gov.uk: - owner: Easingwold Town Council - crown: false - agreement_signed: false -east-ayrshire.gov.uk: - owner: East Ayrshire Council - crown: false - agreement_signed: false -eastbourne.gov.uk: - owner: Eastbourne Borough Council - crown: false - agreement_signed: false -eastcambs.gov.uk: - owner: East Cambridgeshire District Council - crown: false - agreement_signed: false -eastdevon.gov.uk: - owner: East Devon District Council - crown: false - agreement_signed: true -eastdorsetdc.gov.uk: eastdorset.gov.uk -eastdorset.gov.uk: - owner: East Dorset District Council - crown: false - agreement_signed: false -eastdunbarton.gov.uk: - owner: East Dunbartonshire Council - crown: false - agreement_signed: true -eastgrinstead.gov.uk: - owner: East Grinstead Town Council - crown: false - agreement_signed: false -easthants.gov.uk: - owner: East Hampshire District Council - crown: false - agreement_signed: false -eastherts.gov.uk: - owner: East Herts District Council - crown: false - agreement_signed: false -eastkent.gov.uk: thanet.gov.uk -eastleigh.gov.uk: - owner: Eastleigh Borough Council - crown: false - agreement_signed: false -eastlindsey.gov.uk: - owner: East Lindsey District Council - crown: false - agreement_signed: false -eastlothian.gov.uk: - owner: East Lothian Council - crown: false - agreement_signed: false -east-northamptonshire.gov.uk: - owner: East Northamptonshire Council - crown: false - agreement_signed: false -eastrenfrewshire.gov.uk: - owner: East Renfrewshire Council - crown: false - agreement_signed: true -eastriding.gov.uk: - owner: East Riding of Yorkshire Council - crown: false - agreement_signed: true -eaststaffsbc.gov.uk: - owner: East Staffordshire Borough Council - crown: false - agreement_signed: true -eastsuffolk.gov.uk: waveney.gov.uk -eastsussexcc.gov.uk: eastsussex.gov.uk -eastsussex.gov.uk: - owner: East Sussex County Council - crown: false - agreement_signed: true -e-chorley.gov.uk: chorley.gov.uk -edenbridgetowncouncil.gov.uk: - owner: Edenbridge Town Council - crown: false - agreement_signed: false -eden.gov.uk: - owner: Eden District Council - crown: false - agreement_signed: false -edinburgh.gov.uk: - owner: The City of Edinburgh Council - crown: false - agreement_signed: true -eldc.gov.uk: eastlindsey.gov.uk -e-lindsey.gov.uk: eastlindsey.gov.uk -ellesmere-tc.gov.uk: - owner: Ellesmere Town Council - crown: false - agreement_signed: false -elmbridge.gov.uk: - owner: Elmbridge Borough Council - crown: false - agreement_signed: false -elstreeborehamwood-tc.gov.uk: - owner: Elstree & Borehamwood Town Council - crown: false - agreement_signed: false -emergencynorthyorks.gov.uk: northyorks.gov.uk -emersonsgreen-tc.gov.uk: - owner: Emersons Green Town Council - crown: false - agreement_signed: false -encor.gov.uk: corby.gov.uk -enfield.gov.uk: - owner: Enfield London Borough Council - crown: false - agreement_signed: true -eppingforestdc.gov.uk: - owner: Epping Forest District Council - crown: false - agreement_signed: false -eppingtowncouncil.gov.uk: - owner: Epping Town Council - crown: false - agreement_signed: false -epsom-ewell.gov.uk: - owner: Epsom and Ewell Borough Council - crown: false - agreement_signed: false -erewash.gov.uk: - owner: Erewash Borough Council - crown: false - agreement_signed: false -esfrs.org.uk: - owner: East Sussex Fire and Rescue Service - crown: false - agreement_signed: true -essexcc.gov.uk: essex.gov.uk -essex.gov.uk: - owner: Essex County Council - crown: false - agreement_signed: true -essexlegalservices.gov.uk: essex.gov.uk -eveshamtowncouncil.gov.uk: - owner: Evesham Town Council - crown: false - agreement_signed: false -exeterandeastdevon.gov.uk: eastdevon.gov.uk -exeter.gov.uk: - owner: Exeter City Council - crown: false - agreement_signed: false -exmouth.gov.uk: - owner: Exmouth Town Council - crown: false - agreement_signed: false -fairfordtowncouncil.gov.uk: - owner: Fairford Town Council - crown: false - agreement_signed: false -falkirk-council.gov.uk: falkirk.gov.uk -falkirk.gov.uk: - owner: Falkirk Council - crown: false - agreement_signed: true -fareham.gov.uk: - owner: Fareham Borough Council - crown: false - agreement_signed: false -faringdontowncouncil.gov.uk: - owner: Faringdon Town Council - crown: false - agreement_signed: false -farnham.gov.uk: - owner: Farnham Town Council - crown: false - agreement_signed: false -farrier-reg.gov.uk: - owner: The Farriers Registration Council - crown: false - agreement_signed: false -favershamtowncouncil.gov.uk: - owner: Faversham Town Council - crown: false - agreement_signed: false -fdean.gov.uk: forestofdean.gov.uk -featherstone-tc.gov.uk: - owner: Featherstone Town Council - crown: false - agreement_signed: false -felixstowe.gov.uk: - owner: Felixstowe Town Council - crown: false - agreement_signed: false -fenland.gov.uk: - owner: Fenland District Council - crown: false - agreement_signed: false -fennycompton-pc.gov.uk: stratford.gov.uk -ferndown.gov.uk: - owner: Ferndown Town Council - crown: false - agreement_signed: false -fernhurst-wsx-pc.gov.uk: chichester.gov.uk -ferryhill.gov.uk: - owner: Ferryhill Town Council - crown: false - agreement_signed: false -fife.gov.uk: - owner: Fife Council - crown: false - agreement_signed: false -filtontowncouncil.gov.uk: - owner: Filton Town Council - crown: false - agreement_signed: false -fishbourne-pc.gov.uk: chichester.gov.uk -flamborough-pc.gov.uk: eastriding.gov.uk -fleet-tc.gov.uk: - owner: Fleet Town Council - crown: false - agreement_signed: false -flintshire.gov.uk: - owner: Flintshire County Council - crown: false - agreement_signed: false -flitwick.gov.uk: - owner: Flitwick Town Council - crown: false - agreement_signed: false -folkestone-tc.gov.uk: shepway.gov.uk -fordingbridge.gov.uk: newforest.gov.uk -forest-heath.gov.uk: westsuffolk.gov.uk -forestofdean.gov.uk: - owner: Forest Of Dean District Council - crown: false - agreement_signed: false -foweytowncouncil.gov.uk: - owner: Restormel Borough Council - crown: false - agreement_signed: false -frodsham.gov.uk: - owner: Frodsham Town Council - crown: false - agreement_signed: false -frometowncouncil.gov.uk: - owner: Frome Town Council - crown: false - agreement_signed: false -fylde.gov.uk: - owner: Fylde Borough Council - crown: false - agreement_signed: true -galwgofal.gov.uk: conwy.gov.uk -gateshead.gov.uk: - owner: Gateshead Metropolitan Borough Council - crown: false - agreement_signed: true -gedling.gov.uk: - owner: Gedling Borough Council - crown: false - agreement_signed: true -gillinghamdorset-tc.gov.uk: - owner: Gillingham Town Council - crown: false - agreement_signed: false -glamarchives.gov.uk: cardiff.gov.uk -glasgow.gov.uk: - owner: Glasgow City Council - crown: false - agreement_signed: false -glastonbury.gov.uk: - owner: Glastonbury Town Council - crown: false - agreement_signed: false -gloscc.gov.uk: gloucestershire.gov.uk -gloucester.gov.uk: - owner: Gloucester City Council - crown: false - agreement_signed: false -gloucestershire.gov.uk: - owner: Gloucestershire County Council - crown: false - agreement_signed: false -glynneathtowncouncil.gov.uk: - owner: Glynneath Town Council - crown: false - agreement_signed: false -godalming-jbc.gov.uk: - owner: Godalming Town Council - crown: false - agreement_signed: false -godalming-tc.gov.uk: waverley.gov.uk -goole-tc.gov.uk: - owner: Goole Town Council - crown: false - agreement_signed: false -gorseinontowncouncil.gov.uk: - owner: Gorseinon Town Council - crown: false - agreement_signed: false -gosport.gov.uk: - owner: Gosport Borough Council - crown: false - agreement_signed: false -goytre.gov.uk: - owner: Goetre Fawr Community Council - crown: false - agreement_signed: false -grampiancaredata.gov.uk: aberdeenshire.gov.uk -grangeoversandstowncouncil.gov.uk: - owner: Grange Over Sands Town Council - crown: false - agreement_signed: false -gravesham.gov.uk: - owner: Gravesham Borough Council - crown: false - agreement_signed: true -greatalne-pc.gov.uk: stratford.gov.uk -great-aycliffe.gov.uk: - owner: Great Aycliffe Town Council - crown: false - agreement_signed: false -greatbursteadsouthgreenparishcouncil.gov.uk: - owner: Great Burstead and South Green Village Council - crown: false - agreement_signed: false -greatdunmow-tc.gov.uk: - owner: Great Dunmow Town Council - crown: false - agreement_signed: false -great-torringtontowncouncil.gov.uk: - owner: Great Torrington Town Council - crown: false - agreement_signed: false -great-yarmouth.gov.uk: - owner: Great Yarmouth Borough Council - crown: false - agreement_signed: false -guildford.gov.uk: - owner: Guildford Borough Council - crown: false - agreement_signed: false -gwentarchives.gov.uk: blaenau-gwent.gov.uk -gwynedd.gov.uk: - owner: Gwynedd County Council - crown: false - agreement_signed: true -hackney.gov.uk: - owner: Hackney London Borough Council - crown: false - agreement_signed: true -hailsham-tc.gov.uk: - owner: Hailsham Town Council - crown: false - agreement_signed: false -halchereford.gov.uk: - owner: Herefordshire Association of Local Councils - crown: false - agreement_signed: false -halewoodtowncouncil.gov.uk: - owner: Halewood Town Council - crown: false - agreement_signed: false -halton-borough.gov.uk: halton.gov.uk -halton.gov.uk: - owner: Halton Borough Council - crown: false - agreement_signed: true -haltwhistle-tc.gov.uk: - owner: Haltwhistle Town Council - crown: false - agreement_signed: false -hambleton.gov.uk: - owner: Hambleton District Council - crown: false - agreement_signed: false -hampshire.gov.uk: hants.gov.uk -hantsdirect.gov.uk: hants.gov.uk -hants.gov.uk: - owner: Hampshire County Council - crown: false - agreement_signed: true -hantsiowcaddie.gov.uk: hants.gov.uk -hantsnet.gov.uk: hants.gov.uk -hantsweb.gov.uk: hants.gov.uk -harborough.gov.uk: - owner: Harborough District Council - crown: false - agreement_signed: false -haringey.gov.uk: - owner: Haringey London Borough Council - crown: false - agreement_signed: true -harlow.gov.uk: - owner: Harlow District Council - crown: false - agreement_signed: false -harpenden.gov.uk: - owner: Harpenden Town Council - crown: false - agreement_signed: false -harrogate.gov.uk: - owner: Harrogate Borough Council - crown: false - agreement_signed: false -harrow.gov.uk: - owner: Harrow London Borough Council - crown: false - agreement_signed: false -hart.gov.uk: hants.gov.uk -hartlepool.gov.uk: - owner: Hartlepool Borough Council - crown: false - agreement_signed: true -hastings.gov.uk: - owner: Hastings Borough Council - crown: false - agreement_signed: false -hatfield.gov.uk: welwyn.gov.uk -hatfield-herts.gov.uk: - owner: Hatfield Town Council - crown: false - agreement_signed: false -havant.gov.uk: - owner: Havant District Council - crown: false - agreement_signed: false -haverhill-tc.gov.uk: - owner: Haverhill Town Council - crown: false - agreement_signed: false -hawardencommunitycouncil.gov.uk: - owner: Hawarden Community Council - crown: false - agreement_signed: false -hawkinge-tc.gov.uk: - owner: Hawkinge Town Council - crown: false - agreement_signed: false -haxbytowncouncil.gov.uk: - owner: Haxby Town Council - crown: false - agreement_signed: false -haytowncouncil.gov.uk: - owner: Hay Town Council - crown: false - agreement_signed: false -haywardsheath.gov.uk: - owner: Haywards Heath Town Council - crown: false - agreement_signed: false -headley-pc.gov.uk: molevalley.gov.uk -hedgeend-tc.gov.uk: - owner: Hedge End Town Council - crown: false - agreement_signed: false -hednesford-tc.gov.uk: - owner: Hednesford Town Council - crown: false - agreement_signed: false -hedon.gov.uk: - owner: Hedon Town Council - crown: false - agreement_signed: false -helston-tc.gov.uk: - owner: Helston Town Council - crown: false - agreement_signed: false -henley-in-arden-pc.gov.uk: stratford.gov.uk -henleytowncouncil.gov.uk: - owner: Henley-on-Thames Town Council - crown: false - agreement_signed: false -heps.gov.uk: eastriding.gov.uk -herefordcitycouncil.gov.uk: - owner: Hereford City Council - crown: false - agreement_signed: false -herefordshire.gov.uk: - owner: Herefordshire Council - crown: false - agreement_signed: true -hertford.gov.uk: - owner: Hertford Town Council - crown: false - agreement_signed: false -hertfordshire.gov.uk: - owner: Hertfordshire County Council - crown: false - agreement_signed: false -hertscc.gov.uk: hertfordshire.gov.uk -hertsfire.gov.uk: hertfordshire.gov.uk -hertsmere.gov.uk: - owner: Hertsmere Borough Council - crown: false - agreement_signed: false -hertspartnership-ala.gov.uk: eastherts.gov.uk -hessletowncouncil.gov.uk: - owner: Hessle Town Council - crown: false - agreement_signed: false -hettontowncouncil.gov.uk: - owner: Hetton Town Council - crown: false - agreement_signed: false -hexhamtowncouncil.gov.uk: - owner: Hexham Town Council - crown: false - agreement_signed: false -highamferrers-tc.gov.uk: - owner: Higham Ferrers Town Council - crown: false - agreement_signed: false -highland.gov.uk: - owner: The Highland Council - crown: false - agreement_signed: true -highpeak.gov.uk: - owner: High Peak Borough Council - crown: false - agreement_signed: false -highworthtowncouncil.gov.uk: - owner: Highworth Town Council - crown: false - agreement_signed: false -hillingdon.gov.uk: - owner: London Borough of Hillingdon - crown: false - agreement_signed: true -hinckley-bosworth.gov.uk: - owner: Hinckley & Bosworth Borough Council - crown: false - agreement_signed: false -hiow.gov.uk: hants.gov.uk -holmwood-pc.gov.uk: molevalley.gov.uk -holsworthytowncouncil.gov.uk: - owner: Holsworthy Town Council - crown: false - agreement_signed: false -holywell-town.gov.uk: - owner: Holywell Town Council - crown: false - agreement_signed: false -honiton.gov.uk: - owner: Honiton Town Council - crown: false - agreement_signed: false -hopecommunitycouncil.gov.uk: - owner: Hope Community Council - crown: false - agreement_signed: false -horleysurrey-tc.gov.uk: - owner: Horley Town Council - crown: false - agreement_signed: false -hornsea.gov.uk: - owner: Hornsea Town Council - crown: false - agreement_signed: false -horsforthtowncouncil.gov.uk: - owner: Horsforth Town Council - crown: false - agreement_signed: false -horsham.gov.uk: - owner: Horsham District Council - crown: false - agreement_signed: true -horwich.gov.uk: - owner: Horwich Town Council - crown: false - agreement_signed: false -hounslow.gov.uk: - owner: Hounslow London Borough Council - crown: false - agreement_signed: true -howden-tc.gov.uk: - owner: Howden Town Council - crown: false - agreement_signed: false -hullcc.gov.uk: hull.gov.uk -hull.gov.uk: - owner: Kingston Upon Hull City Council - crown: false - agreement_signed: false -humberlrf.gov.uk: eastriding.gov.uk -hungerford-tc.gov.uk: - owner: Hungerford Town Council - crown: false - agreement_signed: false -hunstantontowncouncil.gov.uk: - owner: Hunstanton Town Council - crown: false - agreement_signed: false -huntingdonshire.gov.uk: - owner: Huntingdonshire District Council - crown: false - agreement_signed: false -huntingdontown.gov.uk: - owner: Huntingdon Town Council - crown: false - agreement_signed: false -huntsdc.gov.uk: huntingdonshire.gov.uk -hyde-pc.gov.uk: newforest.gov.uk -hyndburnbc.gov.uk: hyndburn.gov.uk -hyndburn.gov.uk: - owner: Hyndburn Borough Council - crown: false - agreement_signed: false -hytheanddibden.gov.uk: newforest.gov.uk -hythe-tc.gov.uk: hythetowncouncil.gov.uk -hythetowncouncil.gov.uk: - owner: Hythe Town Council - crown: false - agreement_signed: false -ilfracombetowncouncil.gov.uk: - owner: Ilfracombe Town Council - crown: false - agreement_signed: false -ilminster.gov.uk: - owner: Ilminster Town Council - crown: false - agreement_signed: false -immingham-tc.gov.uk: - owner: Immingham Town Council - crown: false - agreement_signed: false -internalauditscotland.gov.uk: west-dunbarton.gov.uk -inverclyde.gov.uk: - owner: Inverclyde Council - crown: false - agreement_signed: true -iow.gov.uk: - owner: Isle of Wight Council - crown: false - agreement_signed: false -ipswich.gov.uk: - owner: Ipswich Borough Council - crown: false - agreement_signed: false -irthlingborough-tc.gov.uk: - owner: Irthlingborough Town Council - crown: false - agreement_signed: false -islington.gov.uk: - owner: Islington London Borough Council - crown: false - agreement_signed: true -isycoedcommunitycouncil.gov.uk: - owner: Isycoed Community Council - crown: false - agreement_signed: false -ivybridge.gov.uk: - owner: Ivybridge Town Council - crown: false - agreement_signed: false -keighley.gov.uk: - owner: Keighley Town Council - crown: false - agreement_signed: false -kendaltowncouncil.gov.uk: - owner: Kendal Town Council - crown: false - agreement_signed: false -kentalc.gov.uk: - owner: Kent Association of Local Councils - crown: false - agreement_signed: false -kentconnects.gov.uk: kent.gov.uk -kent.gov.uk: - owner: Kent County Council - crown: false - agreement_signed: true -kentparishes.gov.uk: kent.gov.uk -kerrier.gov.uk: cornwall.gov.uk -kerrycommunitycouncil.gov.uk: - owner: Kerry Community Council - crown: false - agreement_signed: false -keswicktowncouncil.gov.uk: - owner: Keswick Town Council - crown: false - agreement_signed: false -kettering.gov.uk: - owner: Kettering Borough Council - crown: false - agreement_signed: false -keynsham-tc.gov.uk: - owner: Keynsham Town Council - crown: false - agreement_signed: false -kidderminstertowncouncil.gov.uk: - owner: Kidderminster Town Council - crown: false - agreement_signed: false -kidsgrovetowncouncil.gov.uk: - owner: Kidsgrove Town Council - crown: false - agreement_signed: false -kidwelly.gov.uk: - owner: Kidwelly Town Council - crown: false - agreement_signed: false -kimberley-tc.gov.uk: - owner: Kimberley Town Council - crown: false - agreement_signed: false -kingsbridge.gov.uk: - owner: Kingsbridge Town Council - crown: false - agreement_signed: false -kingston.gov.uk: - owner: Kingston upon Thames London Borough Council - crown: false - agreement_signed: true -kingtontowncouncil.gov.uk: - owner: Kington Town Council - crown: false - agreement_signed: false -kirkbymoorsidetowncouncil.gov.uk: - owner: Kirkbymoorside Town Council - crown: false - agreement_signed: false -kirklees.gov.uk: - owner: Kirklees Council - crown: false - agreement_signed: false -kirtoninlindseytowncouncil.gov.uk: - owner: Kirton in Lindsey Town Council - crown: false - agreement_signed: false -knaresboroughtowncouncil.gov.uk: - owner: Knaresborough Town Council - crown: false - agreement_signed: false -knowsley.gov.uk: - owner: Knowsley Metropolitan Borough Council - crown: false - agreement_signed: true -knowsleytowncouncil.gov.uk: - owner: Knowsley Town Council - crown: false - agreement_signed: false -knutsfordtowncouncil.gov.uk: - owner: Knutsford Town Council - crown: false - agreement_signed: false -lalestoncommunitycouncil.gov.uk: - owner: Laleston Community Council - crown: false - agreement_signed: false -lambeth.gov.uk: - owner: Lambeth London Borough Council - crown: false - agreement_signed: false -lampeter-tc.gov.uk: - owner: Lampeter Town Council - crown: false - agreement_signed: false -lancashire.gov.uk: - owner: Lancashire County Council - crown: false - agreement_signed: true -lancaster.gov.uk: - owner: Lancaster City Council - crown: false - agreement_signed: true -laugharnetownship-wcc.gov.uk: - owner: Laugharne Township Community Council - crown: false - agreement_signed: false -launceston-tc.gov.uk: - owner: Launceston Town Council - crown: false - agreement_signed: false -lawhf.gov.uk: telford.gov.uk -lawleyoverdale-pc.gov.uk: telford.gov.uk -lbbd.gov.uk: barking-dagenham.gov.uk -lbhf.gov.uk: - owner: Hammersmith and Fulham London Borough Council - crown: false - agreement_signed: false -lbwf.gov.uk: walthamforest.gov.uk -leamingtonspatowncouncil.gov.uk: - owner: Royal Leamington Spa Town Council - crown: false - agreement_signed: false -ledburytowncouncil.gov.uk: - owner: Ledbury Town Council - crown: false - agreement_signed: false -leeds.gov.uk: - owner: Leeds City Council - crown: false - agreement_signed: true -leek-tc.gov.uk: leektowncouncil.gov.uk -leektowncouncil.gov.uk: - owner: Leek Town Council - crown: false - agreement_signed: false -leicester.gov.uk: - owner: Leicester City Council - crown: false - agreement_signed: true -leicestershireandrutlandalc.gov.uk: - owner: Leicestershire and Rutland Association of Local Councils - crown: false - agreement_signed: false -leicestershire-fire.gov.uk: leicestershire.gov.uk -leicestershire.gov.uk: - owner: Leicestershire County Council - crown: false - agreement_signed: false -leics.gov.uk: leicestershire.gov.uk -leighonseatowncouncil.gov.uk: - owner: Leigh-on-Sea Town Council - crown: false - agreement_signed: false -leightonlinslade-tc.gov.uk: - owner: Leighton Linslade Town Council - crown: false - agreement_signed: false -leistontowncouncil.gov.uk: - owner: Leiston-cum-Sizewell Town Council - crown: false - agreement_signed: false -leominstertowncouncil.gov.uk: - owner: Leominster Town Council - crown: false - agreement_signed: false -lewes-eastbourne.gov.uk: eastbourne.gov.uk -lewes.gov.uk: - owner: Lewes District Council - crown: false - agreement_signed: false -lewes-tc.gov.uk: - owner: Lewes Town Council - crown: false - agreement_signed: false -lewisham.gov.uk: - owner: Lewisham London Borough Council - crown: false - agreement_signed: true -lg-em.gov.uk: - owner: East Midlands Councils - crown: false - agreement_signed: false -lgyh.gov.uk: wakefield.gov.uk -lichfielddc.gov.uk: - owner: Lichfield District Council - crown: false - agreement_signed: false -lichfield.gov.uk: - owner: Lichfield City Council - crown: false - agreement_signed: false -limavady.gov.uk: - owner: Limavady Borough Council - crown: false - agreement_signed: false -lincoln.gov.uk: - owner: Lincoln City Council - crown: false - agreement_signed: false -lincolnshire.gov.uk: - owner: Lincolnshire County Council - crown: false - agreement_signed: true -lincsbc.gov.uk: eastlindsey.gov.uk -lisburncastlereagh.gov.uk: - owner: Lisburn City Council - crown: false - agreement_signed: false -lisburncityandcastlereagh.gov.uk: lisburncastlereagh.gov.uk -liskeard.gov.uk: - owner: Liskeard Town Council - crown: false - agreement_signed: false -littlechalfontparishcouncil.gov.uk: chiltern.gov.uk -littlechalfont-pc.gov.uk: chiltern.gov.uk -littlehampton-tc.gov.uk: - owner: Littlehampton Town Council - crown: false - agreement_signed: false -liverpoolcityregion-ca.gov.uk: knowsley.gov.uk -liverpool.gov.uk: - owner: Liverpool City Council - crown: false - agreement_signed: false -llanasacommunitycouncil.gov.uk: - owner: Llanasa Community Council - crown: false - agreement_signed: false -llandeilo.gov.uk: - owner: Llandeilo Town Council - crown: false - agreement_signed: false -llandudno.gov.uk: - owner: Cyngor Tref Llandudno Town Council - crown: false - agreement_signed: false -llanelli-rural.gov.uk: - owner: Llanelli Rural Council - crown: false - agreement_signed: false -llanellitowncouncil.gov.uk: - owner: Llanelli Town Council - crown: false - agreement_signed: false -llanfaelogcommunitycouncil.gov.uk: - owner: Llanfaelog Community Council - crown: false - agreement_signed: false -llanfaircommunitycouncil.gov.uk: - owner: Llanfair Community Council - crown: false - agreement_signed: false -llangennechcommunitycouncil.gov.uk: - owner: Llangennech Community Council - crown: false - agreement_signed: false -llangollentowncouncil.gov.uk: - owner: Llangollen Town Council - crown: false - agreement_signed: false -llantwitmajortowncouncil.gov.uk: - owner: Llantwit Major Town Council - crown: false - agreement_signed: false -llwchwrtowncouncil.gov.uk: - owner: Llwchwr Town Council - crown: false - agreement_signed: false -localgovernmentjobsni.gov.uk: ardsandnorthdown.gov.uk -londoncareplacements.gov.uk: londoncouncils.gov.uk -londoncouncils.gov.uk: - owner: London Councils - crown: false - agreement_signed: false -londontribunals.gov.uk: londoncouncils.gov.uk -longcompton-pc.gov.uk: stratford.gov.uk -longridge-tc.gov.uk: - owner: Longridge Town Council - crown: false - agreement_signed: false -looetowncouncil.gov.uk: - owner: Looe Town Council - crown: false - agreement_signed: false -lookinglocal.gov.uk: kirklees.gov.uk -lostwithieltowncouncil.gov.uk: - owner: Lostwithiel Town Council - crown: false - agreement_signed: false -loughton-tc.gov.uk: - owner: Loughton Town Council - crown: false - agreement_signed: false -louthtowncouncil.gov.uk: - owner: Louth Town Council - crown: false - agreement_signed: false -lowestofttowncouncil.gov.uk: - owner: Lowestoft Town Council - crown: false - agreement_signed: false -ludlow.gov.uk: - owner: Ludlow Town Council - crown: false - agreement_signed: false -luton.gov.uk: - owner: Luton Borough Council - crown: false - agreement_signed: true -lymeregistowncouncil.gov.uk: - owner: Lyme Regis Town Council - crown: false - agreement_signed: false -lymingtonandpennington-tc.gov.uk: - owner: Lymington and Pennington Town Council - crown: false - agreement_signed: false -lytchettminsterandupton-tc.gov.uk: - owner: Lytchett Minster & Upton Town Council - crown: false - agreement_signed: false -macclesfield-tc.gov.uk: - owner: Macclesfield Town Council - crown: false - agreement_signed: false -maethugogleddcymru.gov.uk: flintshire.gov.uk -maghull-tc.gov.uk: - owner: Maghull Town Council - crown: false - agreement_signed: false -maiden.gov.uk: gloucestershire.gov.uk -maidstone.gov.uk: - owner: Maidstone Borough Council - crown: false - agreement_signed: true -maldon.gov.uk: - owner: Maldon District Council - crown: false - agreement_signed: false -maldontowncouncil.gov.uk: - owner: Maldon Town Council - crown: false - agreement_signed: false -malmesbury.gov.uk: - owner: Malmesbury Town Council - crown: false - agreement_signed: false -malton-tc.gov.uk: - owner: Malton Town Council - crown: false - agreement_signed: false -malvernhills.gov.uk: - owner: Malvern Hills District Council - crown: false - agreement_signed: false -manchester.gov.uk: - owner: Manchester City Council - crown: false - agreement_signed: true -mansfield-dc.gov.uk: mansfield.gov.uk -mansfield.gov.uk: - owner: Mansfield District Council - crown: false - agreement_signed: true -marchtowncouncil.gov.uk: - owner: March Town Council - crown: false - agreement_signed: false -marketdrayton.gov.uk: - owner: Market Drayton Town Council - crown: false - agreement_signed: false -marketweightontowncouncil.gov.uk: eastriding.gov.uk -marlborough-tc.gov.uk: marlboroughtowncouncil.gov.uk -marlboroughtowncouncil.gov.uk: - owner: Marlborough Town Council - crown: false - agreement_signed: false -marlow-tc.gov.uk: - owner: Marlow Town Council - crown: false - agreement_signed: false -marstonsicca-pc.gov.uk: stratford.gov.uk -maryporttowncouncil.gov.uk: - owner: Maryport Town Council - crown: false - agreement_signed: false -matlock.gov.uk: - owner: Matlock Town Council - crown: false - agreement_signed: false -medway.gov.uk: - owner: Medway Council - crown: false - agreement_signed: true -melthamtowncouncil.gov.uk: - owner: Meltham Town Council - crown: false - agreement_signed: false -melton.gov.uk: - owner: Melton Borough Council - crown: false - agreement_signed: false -mendip.gov.uk: - owner: Mendip District Council - crown: false - agreement_signed: false -merthyr.gov.uk: - owner: Merthyr Tydfil County Borough Council - crown: false - agreement_signed: true -met.pnn.police.uk: - owner: Metropolitan Police Service - crown: false - agreement_signed: true -midandeastantrim.gov.uk: - owner: Mid and East Antrim Borough Council - crown: false - agreement_signed: false -middevon.gov.uk: - owner: Mid Devon District Council - crown: false - agreement_signed: false -middlesbrough.gov.uk: - owner: Middlesbrough Council - crown: false - agreement_signed: true -midhurst-tc.gov.uk: - owner: Midhurst Town Council - crown: false - agreement_signed: false -midkent.gov.uk: swale.gov.uk -midlothian.gov.uk: - owner: Midlothian Council - crown: false - agreement_signed: true -midsuffolk.gov.uk: - owner: Mid Suffolk District Council - crown: false - agreement_signed: false -midsussex.gov.uk: - owner: Mid Sussex District Council - crown: false - agreement_signed: true -milland-wsx-pc.gov.uk: chichester.gov.uk -milton-keynes.gov.uk: miltonkeynes.gov.uk -miltonkeynes.gov.uk: - owner: Milton Keynes Council - crown: false - agreement_signed: true -minstead.gov.uk: newforest.gov.uk -mirfieldtowncouncil.gov.uk: - owner: Mirfield Town Council - crown: false - agreement_signed: false -mole-valley.gov.uk: molevalley.gov.uk -molevalley.gov.uk: - owner: Mole Valley District Council - crown: false - agreement_signed: false -monmouth.gov.uk: - owner: Monmouth Town Council - crown: false - agreement_signed: false -monmouthshire.gov.uk: - owner: Monmouthshire County Council - crown: false - agreement_signed: false -moray-edunet.gov.uk: moray.gov.uk -moray.gov.uk: - owner: Moray Council - crown: false - agreement_signed: false -morecambe.gov.uk: - owner: Morecambe Town Council - crown: false - agreement_signed: false -morley.gov.uk: - owner: Morley Town Council - crown: false - agreement_signed: false -morpeth-tc.gov.uk: - owner: Morpeth Town Council - crown: false - agreement_signed: false -mostyncommunitycouncil.gov.uk: - owner: Mostyn Community Council - crown: false - agreement_signed: false -muchwenlock-tc.gov.uk: - owner: Much Wenlock Town Council - crown: false - agreement_signed: false -mumbles.gov.uk: - owner: Mumbles Community Council - crown: false - agreement_signed: false -mynottingham.gov.uk: nottingham.gov.uk -myoxfordshire.gov.uk: oxfordshire.gov.uk -nafn.gov.uk: tameside.gov.uk -nailseatowncouncil.gov.uk: - owner: Nailsea Town Council - crown: false - agreement_signed: false -nailsworthtowncouncil.gov.uk: - owner: Nailsworth Town Council - crown: false - agreement_signed: false -nalc.gov.uk: - owner: National Association of Local Councils - crown: false - agreement_signed: false -nantwichtowncouncil.gov.uk: - owner: Nantwich Town Council - crown: false - agreement_signed: false -neath-porttalbot.gov.uk: npt.gov.uk -neathtowncouncil.gov.uk: - owner: Neath Town Council - crown: false - agreement_signed: false -ne-derbyshire.gov.uk: - owner: North East Derbyshire District Council - crown: false - agreement_signed: false -ne-ifca.gov.uk: eastriding.gov.uk -nelincs.gov.uk: - owner: North East Lincolnshire Council - crown: false - agreement_signed: true -nelson-mid-glam.gov.uk: - owner: Nelson Community Council - crown: false - agreement_signed: false -nelsontowncouncil.gov.uk: - owner: Nelson Town Council - crown: false - agreement_signed: false -netleymarsh-pc.gov.uk: newforest.gov.uk -newark.gov.uk: - owner: Newark Town Council - crown: false - agreement_signed: false -newark-sherwooddc.gov.uk: - owner: Newark & Sherwood District Council - crown: false - agreement_signed: false -newbiggintowncouncil.gov.uk: - owner: Newbiggin Town Council - crown: false - agreement_signed: false -newbury.gov.uk: - owner: Newbury Town Council - crown: false - agreement_signed: false -newcastleemlyntowncouncil.gov.uk: - owner: Newcastle Emlyn Town Council - crown: false - agreement_signed: false -newcastle.gov.uk: - owner: Newcastle upon Tyne City Council - crown: false - agreement_signed: true -newcastle-staffs.gov.uk: - owner: Newcastle under Lyme Borough Council - crown: false - agreement_signed: true -newenttowncouncil.gov.uk: - owner: Newent Town Council - crown: false - agreement_signed: false -newforestcouncil.gov.uk: newforest.gov.uk -newforestdc.gov.uk: newforest.gov.uk -newforestdistrict.gov.uk: newforest.gov.uk -newforest.gov.uk: - owner: New Forest District Council - crown: false - agreement_signed: false -newham.gov.uk: - owner: Newham London Borough Council - crown: false - agreement_signed: true -newhaventowncouncil.gov.uk: - owner: Newhaven Town Council - crown: false - agreement_signed: false -newmarket.gov.uk: - owner: Newmarket Town Council - crown: false - agreement_signed: false -newmillstowncouncil.gov.uk: - owner: New Mills Town Council - crown: false - agreement_signed: false -newmiltontowncouncil.gov.uk: newforest.gov.uk -newport.gov.uk: - owner: Newport City Council - crown: false - agreement_signed: false -newportshropshire-tc.gov.uk: - owner: Newport Town Council - crown: false - agreement_signed: false -newtonabbot-tc.gov.uk: - owner: Newton Abbot Town Council - crown: false - agreement_signed: false -newtownabbey.gov.uk: - owner: Newtownabbey Borough Council - crown: false - agreement_signed: false -nfdc.gov.uk: newforest.gov.uk -n-kesteven.gov.uk: - owner: North Kesteven District Council - crown: false - agreement_signed: false -norfolkalc.gov.uk: - owner: Norfolk Association of Local Councils - crown: false - agreement_signed: false -norfolkedunet.gov.uk: norfolk.gov.uk -norfolkfireservice.gov.uk: norfolk.gov.uk -norfolk.gov.uk: - owner: Norfolk County Council - crown: false - agreement_signed: false -norfolkparishes.gov.uk: norfolk.gov.uk -norfolkprepared.gov.uk: norfolk.gov.uk -northallertontowncouncil.gov.uk: - owner: Northallerton Town Council - crown: false - agreement_signed: false -northampton.gov.uk: - owner: Northampton Borough Council - crown: false - agreement_signed: false -northamptonshire.gov.uk: - owner: Northamptonshire County Council - crown: false - agreement_signed: false -northamtowncouncil.gov.uk: - owner: Northam Town Council - crown: false - agreement_signed: false -northants-ecl.gov.uk: northamptonshire.gov.uk -northantslicensing.gov.uk: east-northamptonshire.gov.uk -north-ayrshire.gov.uk: - owner: North Ayrshire Council - crown: false - agreement_signed: false -northdevon.gov.uk: - owner: North Devon Council - crown: false - agreement_signed: false -north-dorset.gov.uk: - owner: North Dorset District Council - crown: false - agreement_signed: false -northeastca.gov.uk: newcastle.gov.uk -northeastcouncils.gov.uk: - owner: Association of North East Councils Limited - crown: false - agreement_signed: false -northeast-tadu.gov.uk: gateshead.gov.uk -north-herts.gov.uk: - owner: North Hertfordshire District Council - crown: false - agreement_signed: false -northhykehamtowncouncil.gov.uk: - owner: North Hykeham Town Council - crown: false - agreement_signed: false -northlanarkshire.gov.uk: - owner: North Lanarkshire Council - crown: false - agreement_signed: false -northlan.gov.uk: northlanarkshire.gov.uk -northleach.gov.uk: - owner: Northleach with Eastington Town Council - crown: false - agreement_signed: false -northlincs.gov.uk: - owner: North Lincolnshire Council - crown: false - agreement_signed: true -north-norfolk.gov.uk: - owner: North Norfolk District Council - crown: false - agreement_signed: false -northsomerset.gov.uk: - owner: North Somerset Council - crown: false - agreement_signed: true -northtyneside.gov.uk: - owner: North Tyneside Metropolitan Borough Council - crown: false - agreement_signed: true -northumberland.gov.uk: - owner: Northumberland County Council - crown: false - agreement_signed: true -northwalesadoption.gov.uk: wrexham.gov.uk -northwalesfostering.gov.uk: flintshire.gov.uk -northwarks.gov.uk: - owner: North Warwickshire Borough Council - crown: false - agreement_signed: false -northwestvotes.gov.uk: manchester.gov.uk -northwichtowncouncil.gov.uk: - owner: Northwich Town Council - crown: false - agreement_signed: false -northyorks.gov.uk: - owner: North Yorkshire County Council - crown: false - agreement_signed: true -norwich.gov.uk: - owner: Norwich City Council - crown: false - agreement_signed: false -nottinghamcity.gov.uk: nottingham.gov.uk -nottingham.gov.uk: - owner: Nottingham City Council - crown: false - agreement_signed: true -nottinghamshire.gov.uk: - owner: Nottinghamshire County Council - crown: false - agreement_signed: false -nottscc.gov.uk: nottinghamshire.gov.uk -npt.gov.uk: - owner: Neath Port Talbot County Borough Council - crown: false - agreement_signed: true -n-somerset.gov.uk: northsomerset.gov.uk -nuneatonandbedworth.gov.uk: - owner: Nuneaton and Bedworth Borough Council - crown: false - agreement_signed: false -nwleicestershire.gov.uk: - owner: North West Leicestershire District Council - crown: false - agreement_signed: false -nwleicsdc.gov.uk: nwleicestershire.gov.uk -nwleics.gov.uk: nwleicestershire.gov.uk -oadby-wigston.gov.uk: - owner: Oadby and Wigston Borough Council - crown: false - agreement_signed: false -oakengates-tc.gov.uk: telford.gov.uk -oakhamtowncouncil.gov.uk: - owner: Oakham Town Council - crown: false - agreement_signed: false -offacommunitycouncil.gov.uk: - owner: Offa Community Council - crown: false - agreement_signed: false -okehampton.gov.uk: - owner: Okehampton Town Council - crown: false - agreement_signed: false -oldbolsovertowncouncil.gov.uk: - owner: Old Bolsover Town Council - crown: false - agreement_signed: false -oldham.gov.uk: - owner: Oldham Metropolitan Borough Council - crown: false - agreement_signed: false -ollerton-tc.gov.uk: - owner: Ollerton & Boughton Town Council - crown: false - agreement_signed: false -olneytowncouncil.gov.uk: - owner: Olney Town Council - crown: false - agreement_signed: false -omagh.gov.uk: - owner: Omagh District Council - crown: false - agreement_signed: false -ongartowncouncil.gov.uk: - owner: Ongar Town Council - crown: false - agreement_signed: false -orchardpark.gov.uk: - owner: Orchard Park Community Council - crown: false - agreement_signed: false -orkney.gov.uk: - owner: Orkney Islands Council - crown: false - agreement_signed: true -oswestry-tc.gov.uk: - owner: Oswestry Town Council - crown: false - agreement_signed: false -otleytowncouncil.gov.uk: - owner: Otley Town Council - crown: false - agreement_signed: false -otterystmary-tc.gov.uk: - owner: Ottery St. Mary Town Council - crown: false - agreement_signed: false -oundle.gov.uk: - owner: Oundle Town Council - crown: false - agreement_signed: false -oxford.gov.uk: - owner: Oxford City Council - crown: false - agreement_signed: true -oxfordshire.gov.uk: - owner: Oxfordshire County Council - crown: false - agreement_signed: false -oxfordshire-online.gov.uk: oxfordshire.gov.uk -oxfordshireonline.gov.uk: oxfordshire.gov.uk -oxon.gov.uk: oxfordshire.gov.uk -padihamtowncouncil.gov.uk: - owner: Padiham Town Council - crown: false - agreement_signed: false -padstow-tc.gov.uk: - owner: Padstow Town Council - crown: false - agreement_signed: false -paessex.gov.uk: essex.gov.uk -patchwaytowncouncil.gov.uk: - owner: Patchway Town Council - crown: false - agreement_signed: false -peacehaventowncouncil.gov.uk: - owner: Peacehaven Town Council - crown: false - agreement_signed: false -pembreyburryport-tc.gov.uk: - owner: Pembrey & Burry Port Town Council - crown: false - agreement_signed: false -pembrokeshire.gov.uk: - owner: Pembrokeshire County Council - crown: false - agreement_signed: true -penarthtowncouncil.gov.uk: - owner: Penarth Town Council - crown: false - agreement_signed: false -pencoedtowncouncil.gov.uk: - owner: Pencoed Town Council - crown: false - agreement_signed: false -pendle.gov.uk: - owner: Pendle Borough Council - crown: false - agreement_signed: false -penistonetowncouncil.gov.uk: - owner: Penistone Town Council - crown: false - agreement_signed: false -penworthamtowncouncil.gov.uk: - owner: Penwortham Town Council - crown: false - agreement_signed: false -peoplesnetwork.gov.uk: artscouncil.org.uk -artscouncil.org.uk: - owner: Arts Council England - crown: false - agreement_signed: false -pershore-tc.gov.uk: pershoretowncouncil.gov.uk -pershoretowncouncil.gov.uk: - owner: Pershore Town Council - crown: false - agreement_signed: false -peterborough.gov.uk: - owner: Peterborough City Council - crown: false - agreement_signed: false -peterlee.gov.uk: - owner: Peterlee Town Council - crown: false - agreement_signed: false -petersfield-tc.gov.uk: - owner: Petersfield Town Council - crown: false - agreement_signed: false -petworth-tc.gov.uk: - owner: Petworth Town Council - crown: false - agreement_signed: false -pickering.gov.uk: - owner: Pickering Town Council - crown: false - agreement_signed: false -pitchcombe-pc.gov.uk: stroud.gov.uk -pkc.gov.uk: - owner: Perth & Kinross Council - crown: false - agreement_signed: true -plymouth.gov.uk: - owner: Plymouth City Council - crown: false - agreement_signed: true -plymouthmuseum.gov.uk: plymouth.gov.uk -pocklington.gov.uk: - owner: Pocklington Town Council - crown: false - agreement_signed: false -polegatetowncouncil.gov.uk: - owner: Polegate Town Council - crown: false - agreement_signed: false -polperrocommunitycouncil.gov.uk: - owner: Polperro Community Council - crown: false - agreement_signed: false -pontarddulaistowncouncil.gov.uk: - owner: Pontarddulais Town Council - crown: false - agreement_signed: false -ponteland-tc.gov.uk: - owner: Ponteland Town Council - crown: false - agreement_signed: false -ponthircommunitycouncil.gov.uk: - owner: Ponthir Community Council - crown: false - agreement_signed: false -pontypoolcc.gov.uk: torfaen.gov.uk -pontypriddtowncouncil.gov.uk: - owner: Pontypridd Town Council - crown: false - agreement_signed: false -portishead.gov.uk: - owner: Portishead & North Weston Town Council - crown: false - agreement_signed: false -portlandtowncouncil.gov.uk: - owner: Portland Town Council - crown: false - agreement_signed: false -portsmouthcc.gov.uk: portsmouth.gov.uk -portsmouth.gov.uk: - owner: Portsmouth City Council - crown: false - agreement_signed: true -powys.gov.uk: - owner: Powys County Council - crown: false - agreement_signed: false -poyntontowncouncil.gov.uk: - owner: Poynton Town Council - crown: false - agreement_signed: false -prescot-tc.gov.uk: - owner: Prescot Town Council - crown: false - agreement_signed: false -prestatyntowncouncil.gov.uk: - owner: Prestatyn Town Council - crown: false - agreement_signed: false -preston.gov.uk: - owner: Preston City Council - crown: false - agreement_signed: false -purbeck-dc.gov.uk: purbeck.gov.uk -purbeck.gov.uk: - owner: Purbeck District Council - crown: false - agreement_signed: false -push.gov.uk: hants.gov.uk -queensferrycommunitycouncil.gov.uk: - owner: Queensferry and District Community Council - crown: false - agreement_signed: false -queensparkcommunitycouncil.gov.uk: - owner: Queens Park Community Council - crown: false - agreement_signed: false -radstock-tc.gov.uk: - owner: Radstock Town Council - crown: false - agreement_signed: false -ramseytowncouncil.gov.uk: - owner: Ramsey Town Council - crown: false - agreement_signed: false -ratleyandupton-pc.gov.uk: stratford.gov.uk -raunds-tc.gov.uk: - owner: Raunds Town Council - crown: false - agreement_signed: false -rayleightowncouncil.gov.uk: - owner: Rayleigh Town Council - crown: false - agreement_signed: false -rbkc.gov.uk: - owner: Royal Borough of Kensington and Chelsea - crown: false - agreement_signed: true -rctcbc.gov.uk: rhondda-cynon-taf.gov.uk -reading.gov.uk: - owner: Reading Borough Council - crown: false - agreement_signed: true -readydumgal.gov.uk: dumgal.gov.uk -redbridge.gov.uk: - owner: Redbridge London Borough Council - crown: false - agreement_signed: true -redcar-cleveland.gov.uk: - owner: Redcar and Cleveland Borough Council - crown: false - agreement_signed: true -redditchbc.gov.uk: - owner: Redditch Borough Council - crown: false - agreement_signed: false -redruth-tc.gov.uk: - owner: Redruth Town Council - crown: false - agreement_signed: false -reephamnorfolk-tc.gov.uk: - owner: Reepham Town Council - crown: false - agreement_signed: false -reigate-banstead.gov.uk: - owner: Reigate and Banstead Borough Council - crown: false - agreement_signed: false -renfrewshire.gov.uk: - owner: Renfrewshire Council - crown: false - agreement_signed: false -rhondda-cynon-taff.gov.uk: rhondda-cynon-taf.gov.uk -rhondda-cynon-taf.gov.uk: - owner: Rhondda Cynon Taf County Borough Council - crown: false - agreement_signed: false -rhosdducommunitycouncil.gov.uk: - owner: Rhosddu Community Council - crown: false - agreement_signed: false -rhuddlantowncouncil.gov.uk: denbighshire.gov.uk -ribblevalley.gov.uk: - owner: Ribblevalley Borough Council - crown: false - agreement_signed: false -richmond.gov.uk: richmondandwandsworth.gov.uk -wandsworth.gov.uk: richmondandwandsworth.gov.uk -richmondandwandsworth.gov.uk: - owner: London Boroughs of Richmond and Wandsworth - crown: false - agreement_signed: true -richmondshire.gov.uk: - owner: Richmondshire District Council - crown: false - agreement_signed: false -ringwood.gov.uk: - owner: Ringwood Town Council - crown: false - agreement_signed: false -ripleytowncouncil.gov.uk: - owner: Ripley Town Council - crown: false - agreement_signed: false -riponcity.gov.uk: - owner: Ripon City Council - crown: false - agreement_signed: false -rochdale.gov.uk: - owner: Rochdale Metropolitan Borough Council - crown: false - agreement_signed: false -rochford.gov.uk: - owner: Rochford District Council - crown: false - agreement_signed: false -rossendalebc.gov.uk: rossendale.gov.uk -rossendale.gov.uk: - owner: Rossendale Borough Council - crown: false - agreement_signed: false -rosstc-herefordshire.gov.uk: - owner: Ross-on-Wye Town Council - crown: false - agreement_signed: false -rother.gov.uk: - owner: Rother District Council - crown: false - agreement_signed: true -rotherham.gov.uk: - owner: Rotherham Borough Council - crown: false - agreement_signed: false -royalgreenwich.gov.uk: - owner: Royal Borough of Greenwich - crown: false - agreement_signed: false -royalwoottonbassett.gov.uk: woottonbassett.gov.uk -roystontowncouncil.gov.uk: - owner: Royston Town Council - crown: false - agreement_signed: false -rugby.gov.uk: - owner: Rugby Borough Council - crown: false - agreement_signed: true -rugeleytowncouncil.gov.uk: - owner: Rugeley Town Council - crown: false - agreement_signed: false -runnymede.gov.uk: - owner: Runnymede Borough Council - crown: false - agreement_signed: true -rushcliffe.gov.uk: - owner: Rushcliffe Borough Council - crown: false - agreement_signed: false -rushdentowncouncil.gov.uk: - owner: Rushden Town Council - crown: false - agreement_signed: false -rushmoor.gov.uk: - owner: Rushmoor Borough Council - crown: false - agreement_signed: true -ruthintowncouncil.gov.uk: - owner: Ruthin Town Council - crown: false - agreement_signed: false -rutland.gov.uk: - owner: Rutland County Council - crown: false - agreement_signed: false -rydetowncouncil.gov.uk: - owner: Ryde Town Council - crown: false - agreement_signed: false -ryedale.gov.uk: - owner: Ryedale District Council - crown: false - agreement_signed: true -ryetowncouncil.gov.uk: - owner: Rye Town Council - crown: false - agreement_signed: false -saferderbyshire.gov.uk: derbyshire.gov.uk -saffronwalden.gov.uk: - owner: Saffron Walden Town Council - crown: false - agreement_signed: false -salcombetowncouncil.gov.uk: - owner: Salcombe Town Council - crown: false - agreement_signed: false -salford.gov.uk: - owner: Salford City Council - crown: false - agreement_signed: false -salisburycitycouncil.gov.uk: - owner: Salisbury City Council - crown: false - agreement_signed: false -saltash.gov.uk: - owner: Saltash Town Council - crown: false - agreement_signed: false -saltneytowncouncil.gov.uk: - owner: Saltney Town Council - crown: false - agreement_signed: false -sandbach.gov.uk: - owner: Sandbach Town Council - crown: false - agreement_signed: false -sandhurst.gov.uk: - owner: Sandhurst Town Council - crown: false - agreement_signed: false -sandleheath.gov.uk: newforest.gov.uk -sandowntowncouncil.gov.uk: - owner: Sandown Town Council - crown: false - agreement_signed: false -sandwell.gov.uk: - owner: Sandwell Metropolitan Borough Council - crown: false - agreement_signed: false -sandwichtowncouncil.gov.uk: - owner: Sandwich Town Council - crown: false - agreement_signed: false -sandytowncouncil.gov.uk: - owner: Sandy Town Council - crown: false - agreement_signed: false -sawbridgeworth-tc.gov.uk: - owner: Sawbridgeworth Town Council - crown: false - agreement_signed: false -scambs.gov.uk: - owner: South Cambridgeshire District Council - crown: false - agreement_signed: false -scarborough.gov.uk: - owner: Scarborough Borough Council - crown: false - agreement_signed: false -scilly.gov.uk: - owner: Council of the Isles of Scilly - crown: false - agreement_signed: false -scotborders.gov.uk: - owner: Scottish Borders Council - crown: false - agreement_signed: true -seafordtowncouncil.gov.uk: - owner: Seaford Town Council - crown: false - agreement_signed: false -seaham.gov.uk: - owner: Seaham Town Council - crown: false - agreement_signed: false -sealandcommunitycouncil.gov.uk: - owner: Sealand Community Council - crown: false - agreement_signed: false -seaton.gov.uk: - owner: Seaton Town Council - crown: false - agreement_signed: false -seatonvalleycommunitycouncil.gov.uk: - owner: Seaton Valley Council - crown: false - agreement_signed: false -secouncils.gov.uk: surreycc.gov.uk -sedgefieldtowncouncil.gov.uk: - owner: Sedgefield Town Council - crown: false - agreement_signed: false -sedgemoor.gov.uk: - owner: Sedgemoor District Council - crown: false - agreement_signed: false -sefton.gov.uk: - owner: Sefton Metropolitan Borough Council - crown: false - agreement_signed: true -selby.gov.uk: - owner: Selby District Council - crown: false - agreement_signed: false -selbytowncouncil.gov.uk: selby.gov.uk -selseytowncouncil.gov.uk: - owner: Selsey Town Council - crown: false - agreement_signed: false -sesplan.gov.uk: edinburgh.gov.uk -sevenoaks.gov.uk: - owner: Sevenoaks District Council - crown: false - agreement_signed: true -sevenoakstown.gov.uk: - owner: Sevenoaks Town Council - crown: false - agreement_signed: false -sewta.gov.uk: valeofglamorgan.gov.uk -shaftesbury-tc.gov.uk: - owner: Shaftesbury Town Council - crown: false - agreement_signed: false -sharedresourceservicewales.gov.uk: torfaen.gov.uk -sheffield.gov.uk: - owner: Sheffield City Council - crown: false - agreement_signed: false -sheptonmallettowncouncil.gov.uk: - owner: Shepton Mallet Town Council - crown: false - agreement_signed: false -shepway.gov.uk: - owner: Shepway District Council - crown: false - agreement_signed: false -sherborne-tc.gov.uk: - owner: Sherborne Town Council - crown: false - agreement_signed: false -shetland.gov.uk: - owner: Shetland Islands Council - crown: false - agreement_signed: false -shetland-library.gov.uk: shetland.gov.uk -shifnaltowncouncil.gov.uk: - owner: Shifnal Town Council - crown: false - agreement_signed: false -shildon.gov.uk: - owner: Shildon Town Council - crown: false - agreement_signed: false -shirebrooktowncouncil.gov.uk: - owner: Shirebrook Town Council - crown: false - agreement_signed: false -sholland.gov.uk: - owner: South Holland District Council - crown: false - agreement_signed: false -shrewsburytowncouncil.gov.uk: - owner: Shrewsbury Town Council - crown: false - agreement_signed: false -shropshire.gov.uk: - owner: Shropshire Council - crown: false - agreement_signed: false -sidmouth.gov.uk: - owner: Sidmouth Town Council - crown: false - agreement_signed: false -sigoma.gov.uk: barnsley.gov.uk -silloth-on-solway-tc.gov.uk: - owner: Silloth-on-Solway Town Council - crown: false - agreement_signed: false -sir-benfro.gov.uk: pembrokeshire.gov.uk -sirddinbych.gov.uk: denbighshire.gov.uk -sirfynwy.gov.uk: monmouthshire.gov.uk -sirgaerfyrddin.gov.uk: carmarthenshire.gov.wales -sirgar.gov.uk: carmarthenshire.gov.wales -siryfflint.gov.uk: flintshire.gov.uk -skegness.gov.uk: - owner: Skegness Town Council - crown: false - agreement_signed: false -skiptontowncouncil.gov.uk: - owner: Skipton Town Council - crown: false - agreement_signed: false -sleaford.gov.uk: - owner: Sleaford Town Council - crown: false - agreement_signed: false -slough.gov.uk: - owner: Slough Borough Council - crown: false - agreement_signed: false -s-norfolk.gov.uk: south-norfolk.gov.uk -sodburytowncouncil.gov.uk: - owner: Sodbury Town Council - crown: false - agreement_signed: false -soham-tc.gov.uk: - owner: Soham Town Council - crown: false - agreement_signed: false -solihull.gov.uk: - owner: Solihull Metropolitan Borough Council - crown: false - agreement_signed: true -somerset.gov.uk: - owner: Somerset County Council - crown: false - agreement_signed: true -somertontowncouncil.gov.uk: - owner: Somerton Town Council - crown: false - agreement_signed: false -sopley.gov.uk: newforest.gov.uk -southamcouncil-warks.gov.uk: - owner: Southam Town Council - crown: false - agreement_signed: false -southampton.gov.uk: - owner: Southampton City Council - crown: false - agreement_signed: false -southandvale.gov.uk: whitehorsedc.gov.uk -south-ayrshire.gov.uk: - owner: South Ayrshire Council - crown: false - agreement_signed: true -southbucks.gov.uk: - owner: South Buckinghamshire District Council - crown: false - agreement_signed: true -south-derbys.gov.uk: - owner: South Derbyshire District Council - crown: false - agreement_signed: false -southend.gov.uk: - owner: Southend-on-Sea Borough Council - crown: false - agreement_signed: true -southernuplandway.gov.uk: dumgal.gov.uk -southglos.gov.uk: - owner: South Gloucestershire Council - crown: false - agreement_signed: true -south-hams-dc.gov.uk: southhams.gov.uk -southhams.gov.uk: - owner: South Hams District Council - crown: false - agreement_signed: false -southkesteven.gov.uk: - owner: South Kesteven District Council - crown: false - agreement_signed: false -southkirkbyandmoorthorpetowncouncil.gov.uk: - owner: South Kirkby and Moorthorpe Town Council - crown: false - agreement_signed: false -southlakeland.gov.uk: - owner: South Lakeland District Council - crown: false - agreement_signed: false -southlanarkshire.gov.uk: - owner: South Lanarkshire Council - crown: false - agreement_signed: true -south-norfolk.gov.uk: - owner: South Norfolk Council - crown: false - agreement_signed: false -southnorthants.gov.uk: - owner: South Northamptonshire Council - crown: false - agreement_signed: false -southoxon.gov.uk: - owner: South Oxfordshire District Council - crown: false - agreement_signed: false -southribble.gov.uk: - owner: South Ribble Borough Council - crown: false - agreement_signed: false -southribbletourism.gov.uk: southribble.gov.uk -southsomerset.gov.uk: - owner: South Somerset District Council - crown: false - agreement_signed: false -southtyneside.gov.uk: - owner: South Tyneside Council - crown: false - agreement_signed: true -southwark.gov.uk: - owner: Southwark London Borough Council - crown: false - agreement_signed: true -southwell-tc.gov.uk: - owner: Southwell Town Council - crown: false - agreement_signed: false -southwest-ra.gov.uk: somerset.gov.uk -southwoodhamferrerstc.gov.uk: - owner: South Woodham Ferrers Town Council - crown: false - agreement_signed: false -southworcestershirebuildingcontrol.gov.uk: malvernhills.gov.uk -southworcestershirerevenues.gov.uk: wychavon.gov.uk -spelthorne.gov.uk: - owner: Spelthorne Borough Council - crown: false - agreement_signed: false -spennymoor-tc.gov.uk: - owner: Spennymoor Town Council - crown: false - agreement_signed: false -sprowston-tc.gov.uk: - owner: Sprowston Town Council - crown: false - agreement_signed: false -sstaffs.gov.uk: - owner: South Staffordshire District Council - crown: false - agreement_signed: false -staffordbc.gov.uk: - owner: Stafford Borough Council - crown: false - agreement_signed: false -staffordshire.gov.uk: - owner: Staffordshire County Council - crown: false - agreement_signed: true -staffordshireprepared.gov.uk: staffordshire.gov.uk -staffs.gov.uk: staffordshire.gov.uk -staffslc.gov.uk: staffordshire.gov.uk -staffsmoorlands.gov.uk: - owner: Staffordshire Moorlands District Council - crown: false - agreement_signed: false -stainforthtowncouncil.gov.uk: - owner: Stainforth Town Council - crown: false - agreement_signed: false -stalbans.gov.uk: - owner: St Albans District Council - crown: false - agreement_signed: false -stalbridgetowncouncil.gov.uk: - owner: Stalbridge Town Council - crown: false - agreement_signed: false -stamfordtowncouncil.gov.uk: - owner: Stamford Town Council - crown: false - agreement_signed: false -stanley-tc.gov.uk: - owner: Stanley Town Council - crown: false - agreement_signed: false -stannesonthesea-tc.gov.uk: - owner: St. Annes on the Sea Town Council - crown: false - agreement_signed: false -star-procurement.gov.uk: trafford.gov.uk -stasaphcitycouncil.gov.uk: - owner: St Asaph City Council - crown: false - agreement_signed: false -staustell-tc.gov.uk: - owner: St Austell Town Council - crown: false - agreement_signed: false -staveleytowncouncil.gov.uk: - owner: Staveley Town Council - crown: false - agreement_signed: false -stcolumbmajor-tc.gov.uk: - owner: St Columb Major Town Council - crown: false - agreement_signed: false -stdavids.gov.uk: - owner: St. David's City Council - crown: false - agreement_signed: false -stdonats-cc.gov.uk: - owner: St Donats communitiy Council - crown: false - agreement_signed: false -stedmundsbury.gov.uk: westsuffolk.gov.uk -stedsbc.gov.uk: westsuffolk.gov.uk -stevenage.gov.uk: - owner: Stevenage Borough Council - crown: false - agreement_signed: false -stharmoncommunitycouncil.gov.uk: - owner: St Harmon Community Council - crown: false - agreement_signed: false -sthelens.gov.uk: - owner: St Helens Metropolitan Borough Council - crown: false - agreement_signed: true -stirling.gov.uk: - owner: Stirling Council - crown: false - agreement_signed: true -stives-tc.gov.uk: stivestowncouncil.gov.uk -stivestowncouncil-cornwall.gov.uk: stivestowncouncil.gov.uk -stivestowncouncil.gov.uk: - owner: St Ives Town Council - crown: false - agreement_signed: false -stmarybourne-pc.gov.uk: basingstoke.gov.uk -stneots-tc.gov.uk: - owner: St Neots Town Council - crown: false - agreement_signed: false -stock-pc.gov.uk: - owner: Stock Village Council - crown: false - agreement_signed: false -stockport.gov.uk: - owner: Stockport Metropolitan Borough Council - crown: false - agreement_signed: true -stockportmbc.gov.uk: stockport.gov.uk -stockton.gov.uk: - owner: Stockton-on-Tees Borough Council - crown: false - agreement_signed: false -stockton-warks-pc.gov.uk: stratford.gov.uk -stoke.gov.uk: - owner: Stoke-on-Trent City Council - crown: false - agreement_signed: true -stonetowncouncil.gov.uk: - owner: Stone Town Council - crown: false - agreement_signed: false -stonystratford.gov.uk: - owner: Stony Stratford Town Council - crown: false - agreement_signed: false -stoploansharks.gov.uk: birmingham.gov.uk -stotfoldtowncouncil.gov.uk: - owner: Stotfold Town Council - crown: false - agreement_signed: false -stourporttowncouncil.gov.uk: - owner: Stourport-on-Severn Town Council - crown: false - agreement_signed: false -stowonthewold-tc.gov.uk: - owner: Stow-on-the-Wold Town Council - crown: false - agreement_signed: false -stratford-dc.gov.uk: stratford.gov.uk -stratford.gov.uk: - owner: Stratford-on-Avon District Council - crown: false - agreement_signed: false -stratford-tc.gov.uk: - owner: Stratford-upon-Avon Town Council - crown: false - agreement_signed: false -stroud.gov.uk: - owner: Stroud District Council - crown: false - agreement_signed: false -stroudtown.gov.uk: - owner: Stroud Town Council - crown: false - agreement_signed: false -sturminsternewton-tc.gov.uk: - owner: Sturminster Newton Town Council - crown: false - agreement_signed: false -s-tyneside-mbc.gov.uk: southtyneside.gov.uk -suffolkcc.gov.uk: suffolk.gov.uk -suffolkcoastal.gov.uk: suffolk.gov.uk -suffolk.gov.uk: - owner: Suffolk County Council - crown: false - agreement_signed: false -sunderland.gov.uk: - owner: Sunderland City Metropolitan Borough Council - crown: false - agreement_signed: false -supportwithconfidence.gov.uk: surreycc.gov.uk -surreycc.gov.uk: - owner: Surrey County Council - crown: false - agreement_signed: false -surreycoroner.gov.uk: surreycc.gov.uk -surreycountycouncil.gov.uk: surreycc.gov.uk -surrey-fire.gov.uk: surreycc.gov.uk -surrey.gov.uk: surreycc.gov.uk -surreyheath.gov.uk: - owner: Surrey Heath Borough Council - crown: false - agreement_signed: false -surreyheath-online.gov.uk: surreyheath.gov.uk -surreyheathonline.gov.uk: surreyheath.gov.uk -surreyi.gov.uk: surreycc.gov.uk -surreylocalgovernment.gov.uk: woking.gov.uk -sussexsaferroads.gov.uk: westsussex.gov.uk -suttoncoldfieldtowncouncil.gov.uk: - owner: Sutton Coldfield Town Council - crown: false - agreement_signed: false -sutton.gov.uk: - owner: Sutton London Borough Council - crown: false - agreement_signed: false -swaffhamtowncouncil.gov.uk: - owner: Swaffham Town Council - crown: false - agreement_signed: false -swale.gov.uk: - owner: Swale Borough Council - crown: false - agreement_signed: false -swanage.gov.uk: - owner: Swanage Town Council - crown: false - agreement_signed: false -swanleytowncouncil.gov.uk: - owner: Swanley Town Council - crown: false - agreement_signed: false -swanscombeandgreenhithetowncouncil.gov.uk: - owner: Swanscombe & Greenhithe Town Council - crown: false - agreement_signed: false -sway-pc.gov.uk: newforest.gov.uk -swdevon.gov.uk: southhams.gov.uk -swindon.gov.uk: - owner: Swindon Borough Council - crown: false - agreement_signed: false -tadleytowncouncil.gov.uk: - owner: Tadley Town Council - crown: false - agreement_signed: false -tameside.gov.uk: - owner: Tameside Metropolitan Borough Council - crown: false - agreement_signed: true -tamworth.gov.uk: - owner: Tamworth Borough Council - crown: false - agreement_signed: false -tandridgedc.gov.uk: tandridge.gov.uk -tandridge.gov.uk: - owner: Tandridge District Council - crown: false - agreement_signed: false -tauntondeane.gov.uk: - owner: Taunton Deane Borough Council - crown: false - agreement_signed: false -tavistock.gov.uk: - owner: Tavistock Town Council - crown: false - agreement_signed: false -tayplan-sdpa.gov.uk: dundeecity.gov.uk -taysideprocurement.gov.uk: dundeecity.gov.uk -teesvalley-ca.gov.uk: stockton.gov.uk -teesvalleyunlimited.gov.uk: stockton.gov.uk -teignbridge.gov.uk: - owner: Teignbridge District Council - crown: false - agreement_signed: true -teignmouth-devon.gov.uk: - owner: Teignmouth Town Council - crown: false - agreement_signed: false -telford.gov.uk: - owner: Telford & Wrekin Council - crown: false - agreement_signed: false -telscombetowncouncil.gov.uk: - owner: Telscombe Town Council - crown: false - agreement_signed: false -tendringdc.gov.uk: - owner: Tendring District Council - crown: false - agreement_signed: false -tenterdentowncouncil.gov.uk: - owner: Tenterden Town Council - crown: false - agreement_signed: false -testvalley.gov.uk: - owner: Test Valley Borough Council - crown: false - agreement_signed: true -tetbury.gov.uk: - owner: Tetbury Town Council - crown: false - agreement_signed: false -tewkesburybc.gov.uk: tewkesbury.gov.uk -tewkesbury.gov.uk: - owner: Tewkesbury Borough Council - crown: false - agreement_signed: false -tewkesburytowncouncil.gov.uk: - owner: Tewkesbury Town Council - crown: false - agreement_signed: false -thametowncouncil.gov.uk: - owner: Thame Town Council - crown: false - agreement_signed: false -thanet.gov.uk: - owner: Thanet District Council - crown: false - agreement_signed: false -thatchamtowncouncil.gov.uk: - owner: Thatcham Town Council - crown: false - agreement_signed: false -themcmanus-dundee.gov.uk: dundeecity.gov.uk -thetfordtowncouncil.gov.uk: - owner: Thetford Town Council - crown: false - agreement_signed: false -thirsk-tc.gov.uk: - owner: Thirsk Town Council - crown: false - agreement_signed: false -thornabytowncouncil.gov.uk: - owner: Thornaby Town Council - crown: false - agreement_signed: false -thornburytowncouncil.gov.uk: - owner: Thornbury Town Council - crown: false - agreement_signed: false -thorne-moorends.gov.uk: - owner: Thorne Moorends Town Council - crown: false - agreement_signed: false -thorpestandrew-tc.gov.uk: - owner: Thorpe St Andrew Town Council - crown: false - agreement_signed: false -thrapstontowncouncil.gov.uk: - owner: Thrapston Town Council - crown: false - agreement_signed: false -threerivers.gov.uk: - owner: Three Rivers District Council - crown: false - agreement_signed: false -thurrockfostering.gov.uk: thurrock.gov.uk -thurrock.gov.uk: - owner: Thurrock Council - crown: false - agreement_signed: false -tidworthtowncouncil.gov.uk: - owner: Tidworth Town Council - crown: false - agreement_signed: false -tivertontowncouncil.gov.uk: - owner: Tiverton Town Council - crown: false - agreement_signed: false -tmbc.gov.uk: - owner: Tonbridge & Malling Borough Council - crown: false - agreement_signed: true -tobaccoregisterni.gov.uk: belfastcity.gov.uk -torbay.gov.uk: - owner: Torbay Council - crown: false - agreement_signed: false -torfaen.gov.uk: - owner: Torfaen County Borough Council - crown: false - agreement_signed: true -torpointtowncouncil.gov.uk: - owner: Torpoint Town Council - crown: false - agreement_signed: false -torridge.gov.uk: - owner: Torridge District Council - crown: false - agreement_signed: true -totnestowncouncil.gov.uk: - owner: Totnes Town Council - crown: false - agreement_signed: false -totton-and-eling-tc.gov.uk: - owner: Totton and Eling Town Council - crown: false - agreement_signed: false -towcester-tc.gov.uk: - owner: Towcester Town Council - crown: false - agreement_signed: false -towerhamlets.gov.uk: - owner: Tower Hamlets London Borough Council - crown: false - agreement_signed: true -towynkinmelbay-tc.gov.uk: - owner: Towyn and Kinmel Bay Town Council - crown: false - agreement_signed: false -tracc.gov.uk: powys.gov.uk -trafford.gov.uk: - owner: Trafford Metropolitan Borough Council - crown: false - agreement_signed: true -traffordparishes.gov.uk: trafford.gov.uk -tring.gov.uk: - owner: Tring Town Council - crown: false - agreement_signed: false -trowbridge.gov.uk: - owner: Trowbridge Town Council - crown: false - agreement_signed: false -truro-city.gov.uk: truro.gov.uk -truro.gov.uk: - owner: Truro City Council - crown: false - agreement_signed: false -tunbridgewells.gov.uk: - owner: Tunbridge Wells Borough Council - crown: false - agreement_signed: true -twfire.gov.uk: - owner: Tyne and Wear Fire and Rescue Authority - crown: false - agreement_signed: true -twict.gov.uk: sunderland.gov.uk -tyneandwearltp.gov.uk: newcastle.gov.uk -uckfieldtc.gov.uk: - owner: Uckfield Town Council - crown: false - agreement_signed: false -uppinghamtowncouncil.gov.uk: - owner: Uppingham Town Council - crown: false - agreement_signed: false -uptonstleonards-pc.gov.uk: stroud.gov.uk -uttlesford.gov.uk: - owner: Uttlesford District Council - crown: false - agreement_signed: false -valeofglamorgan.gov.uk: - owner: Vale of Glamorgan Council - crown: false - agreement_signed: true -verwood.gov.uk: - owner: Verwood Town Council - crown: false - agreement_signed: false -visitsaffronwalden.gov.uk: saffronwalden.gov.uk -visitsouthribble.gov.uk: southribble.gov.uk -wadebridge-tc.gov.uk: - owner: Wadebridge Town Council - crown: false - agreement_signed: false -wakefield.gov.uk: - owner: Wakefield City Council - crown: false - agreement_signed: false -wallingfordtowncouncil.gov.uk: - owner: Wallingford Town Council - crown: false - agreement_signed: false -walsall.gov.uk: - owner: Walsall Metropolitan Borough Council - crown: false - agreement_signed: true -walthamabbey-tc.gov.uk: - owner: Waltham Abbey Town Council - crown: false - agreement_signed: false -walthamforestclass.gov.uk: walthamforest.gov.uk -walthamforest.gov.uk: - owner: Waltham Forest London Borough Council - crown: false - agreement_signed: false -waltoncommunitycouncil.gov.uk: - owner: Walton Community Council - crown: false - agreement_signed: false -wantagetowncouncil.gov.uk: - owner: Wantage Town Council - crown: false - agreement_signed: false -wareham-tc.gov.uk: - owner: Wareham Town Council - crown: false - agreement_signed: false -waretowncouncil.gov.uk: - owner: Ware Town Council - crown: false - agreement_signed: false -warminster-tc.gov.uk: - owner: Warminster Town Council - crown: false - agreement_signed: false -warrington.gov.uk: - owner: Warrington Borough Council - crown: false - agreement_signed: true -warwick-dc.gov.uk: warwick.gov.uk -warwickdc.gov.uk: warwick.gov.uk -warwick.gov.uk: - owner: Warwick District Council - crown: false - agreement_signed: false -warwickshire.gov.uk: - owner: Warwickshire County Council - crown: false - agreement_signed: true -warwicktowncouncil.gov.uk: - owner: Warwick Town Council - crown: false - agreement_signed: false -watford-council.gov.uk: watford.gov.uk -watford.gov.uk: - owner: Watford Borough Council - crown: false - agreement_signed: false -wattontowncouncil.gov.uk: - owner: Watton Town Council - crown: false - agreement_signed: false -waveney.gov.uk: - owner: Waveney District Council - crown: false - agreement_signed: false -waverley.gov.uk: - owner: Waverley Borough Council - crown: false - agreement_signed: false -wealden.gov.uk: - owner: Wealden District Council - crown: false - agreement_signed: true -welfordonavon-pc.gov.uk: stratford.gov.uk -welhat.gov.uk: welwyn.gov.uk -wellingborough.gov.uk: - owner: Wellingborough Borough Council - crown: false - agreement_signed: false -wellington-shropshire.gov.uk: - owner: Wellington Town Council - crown: false - agreement_signed: false -wells.gov.uk: - owner: Wells City Council - crown: false - agreement_signed: false -welshpooltowncouncil.gov.uk: - owner: Welshpool Town Council - crown: false - agreement_signed: false -welwyn.gov.uk: - owner: Welwyn Hatfield Borough Council - crown: false - agreement_signed: false -wem.gov.uk: - owner: Wem Town Council - crown: false - agreement_signed: false -westberks.gov.uk: - owner: West Berkshire Council - crown: false - agreement_signed: false -westbletchleycouncil.gov.uk: - owner: West Bletchley Council - crown: false - agreement_signed: false -westburytowncouncil.gov.uk: - owner: Westbury Town Council - crown: false - agreement_signed: false -westcumbria.gov.uk: allerdale.gov.uk -westdevon.gov.uk: - owner: West Devon Borough Council - crown: false - agreement_signed: false -westdorset-dc.gov.uk: dorset.gov.uk -westdorset-weymouth.gov.uk: - owner: West Dorset District Council - crown: false - agreement_signed: false -west-dunbarton.gov.uk: - owner: West Dunbartonshire Council - crown: false - agreement_signed: true -westerhamtowncouncil.gov.uk: - owner: Westerham Town Council - crown: false - agreement_signed: false -westgateonsea.gov.uk: - owner: Westgate-On-Sea Town Council - crown: false - agreement_signed: false -westlancashire.gov.uk: - owner: West Lancashire District Council - crown: false - agreement_signed: false -westlancsdc.gov.uk: westlancashire.gov.uk -westlancs.gov.uk: westlancashire.gov.uk -west-lindsey.gov.uk: - owner: West Lindsey District Council - crown: false - agreement_signed: false -westlothian.gov.uk: - owner: West Lothian Council - crown: false - agreement_signed: true -westmidlandsca.gov.uk: wolverhampton.gov.uk -westminster.gov.uk: - owner: Westminster City Council - crown: false - agreement_signed: false -west-norfolk.gov.uk: - owner: Borough Council of King's Lynn & West Norfolk - crown: false - agreement_signed: false -westofengland-ca.gov.uk: bathnes.gov.uk -westoxon.gov.uk: - owner: West Oxfordshire District Council - crown: false - agreement_signed: false -westsomerset.gov.uk: - owner: West Somerset District Council - crown: false - agreement_signed: false -westsomersetonline.gov.uk: westsomerset.gov.uk -westsuffolk.gov.uk: - owner: St Edmundsbury Borough Council - crown: false - agreement_signed: true -westsussex.gov.uk: - owner: West Sussex County Council - crown: false - agreement_signed: true -westwey.gov.uk: weymouth.gov.uk -westyorkshire-pcp.gov.uk: wakefield.gov.uk -weymouth.gov.uk: - owner: Weymouth and Portland Borough Council - crown: false - agreement_signed: false -whiston-tc.gov.uk: - owner: Whiston Town Council - crown: false - agreement_signed: false -whitbytowncouncil.gov.uk: - owner: Whitby Town Council - crown: false - agreement_signed: false -whitchurch-hampshire-tc.gov.uk: - owner: Whitchurch Town Council - crown: false - agreement_signed: false -whitehilltowncouncil.gov.uk: - owner: Whitehill Town Council - crown: false - agreement_signed: false -whitehorsedc.gov.uk: - owner: Vale of White Horse District Council - crown: false - agreement_signed: false -whitnashtowncouncil.gov.uk: - owner: Whitnash Town Council - crown: false - agreement_signed: false -whitworth.gov.uk: - owner: Whitworth Town Council - crown: false - agreement_signed: false -wigan.gov.uk: - owner: Wigan Metropolitan Borough Council - crown: false - agreement_signed: true -wiganmbc.gov.uk: wigan.gov.uk -wiltontowncouncil.gov.uk: - owner: Wilton Town Council - crown: false - agreement_signed: false -wiltshire.gov.uk: - owner: The Wiltshire Council - crown: false - agreement_signed: true -wimborne.gov.uk: - owner: Wimborne Minster Town Council - crown: false - agreement_signed: false -winchester.gov.uk: - owner: Winchester City Council - crown: false - agreement_signed: false -windermere-tc.gov.uk: - owner: Windermere Town Council - crown: false - agreement_signed: false -winsford.gov.uk: - owner: Winsford Town Council - crown: false - agreement_signed: false -winslowtowncouncil.gov.uk: - owner: Winslow Town Council - crown: false - agreement_signed: false -wirksworth.gov.uk: - owner: Wirksworth Town Council - crown: false - agreement_signed: false -wirksworthtowncouncil.gov.uk: wirksworth.gov.uk -wirral-mbc.gov.uk: wirral.gov.uk -wirral.gov.uk: - owner: Wirral Metropolitan Borough Council - crown: false - agreement_signed: true -wisbechtowncouncil.gov.uk: - owner: Wisbech Town Council - crown: false - agreement_signed: false -witham.gov.uk: - owner: Witham Town Council - crown: false - agreement_signed: false -witney-tc.gov.uk: - owner: Witney Town Council - crown: false - agreement_signed: false -wivenhoe.gov.uk: - owner: Wivenhoe Town Council - crown: false - agreement_signed: false -wixford-pc.gov.uk: stratford.gov.uk -wmfs.net: - owner: West Midlands Fire Service - crown: false - agreement_signed: true -woking.gov.uk: - owner: Woking Borough Council - crown: false - agreement_signed: true -wokingham.gov.uk: - owner: Wokingham Borough Council - crown: false - agreement_signed: true -wokingham-tc.gov.uk: - owner: Wokingham Town Council - crown: false - agreement_signed: false -wolverhampton.gov.uk: - owner: Wolverhampton City Council - crown: false - agreement_signed: true -wolvertonandgreenleystowncouncil.gov.uk: - owner: Wolverton and Greenleys Town Council - crown: false - agreement_signed: false -woodbridge-suffolk.gov.uk: - owner: Woodbridge Town Council - crown: false - agreement_signed: false -woodley.gov.uk: - owner: Woodley Town Council - crown: false - agreement_signed: false -woodstock-tc.gov.uk: - owner: Woodstock Town Council - crown: false - agreement_signed: false -woottonbassett.gov.uk: - owner: Wootton Bassett Town Council - crown: false - agreement_signed: false -woottonwawen-pc.gov.uk: stratford.gov.uk -worcester.gov.uk: - owner: City of Worcester Council - crown: false - agreement_signed: false -worcestershire.gov.uk: - owner: Worcestershire County Council - crown: false - agreement_signed: true -worcestershirehub.gov.uk: worcestershire.gov.uk -worcestershirets.gov.uk: worcestershire.gov.uk -worcsregservices.gov.uk: bromsgrove.gov.uk -workingtontowncouncil.gov.uk: - owner: Workington Town Council - crown: false - agreement_signed: false -worthing.gov.uk: - owner: Worthing Borough Council - crown: false - agreement_signed: false -wra.gov.wales: - owner: Welsh Revenue Authority - crown: false - agreement_signed: true -wrecsam.gov.uk: wrexham.gov.uk -wrexham.gov.uk: - owner: Wrexham County Borough Council - crown: false - agreement_signed: true -wsm-tc.gov.uk: - owner: Weston-super-Mare Town Council - crown: false - agreement_signed: false -wsx-donnington-pc.gov.uk: chichester.gov.uk -wychavon.gov.uk: - owner: Wychavon District Council - crown: false - agreement_signed: false -wycombe.gov.uk: - owner: Wycombe District Council - crown: false - agreement_signed: false -wyrebc.gov.uk: - owner: Wyre Borough Council - crown: false - agreement_signed: false -wyreforestdc.gov.uk: wyreforest.gov.uk -wyreforest.gov.uk: - owner: Wyre Forest District Council - crown: false - agreement_signed: true -wyre.gov.uk: - owner: Wyre Council - crown: false - agreement_signed: false -yateley-tc.gov.uk: - owner: Yateley Town Council - crown: false - agreement_signed: false -yatetowncouncil.gov.uk: - owner: Yate Town Council - crown: false - agreement_signed: false -yeovil.gov.uk: southsomerset.gov.uk -yeoviltonparishcouncil.gov.uk: - owner: Yeovil Town Council - crown: false - agreement_signed: false -ynysmon.gov.uk: anglesey.gov.uk -york.gov.uk: - owner: City of York Council - crown: false - agreement_signed: true -yorkshirelca.gov.uk: - owner: Yorkshire Local Councils Associations - crown: false - agreement_signed: false From 07c754404972f921afc0f0ee2caa767110c4603b Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 4 Apr 2019 15:02:30 +0100 Subject: [PATCH 16/31] Use crown not crown_status as prop name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s confusing having a property name which is different from the attribute returned in the JSON from the API. --- app/models/organisation.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/models/organisation.py b/app/models/organisation.py index e3f827ff7..c9cdef32b 100644 --- a/app/models/organisation.py +++ b/app/models/organisation.py @@ -27,10 +27,6 @@ class Organisation(JSONModel): if self._dict == {}: self.name, self.crown, self.agreement_signed = None, None, None - @property - def crown_status(self): - return self.crown - def as_human_readable(self, fallback_domain): if 'dwp.' in ''.join(self.domains): return 'DWP - Requires OED approval' @@ -47,7 +43,7 @@ class Organisation(JSONModel): True: 'a crown body', False: 'a non-crown body', None: 'crown status unknown', - }.get(self.crown_status), + }.get(self.crown), ) else: return 'Can’t tell (domain is {})'.format(fallback_domain) @@ -57,7 +53,7 @@ class Organisation(JSONModel): @property def as_jinja_template(self): - if self.crown_status is None: + if self.crown is None: return 'agreement-choose' if self.agreement_signed: return 'agreement-signed' @@ -133,6 +129,6 @@ class Organisation(JSONModel): @property def crown_status_or_404(self): - if self.crown_status is None: + if self.crown is None: abort(404) - return self.crown_status + return self.crown From 3212915a5126d94e1404f120af3c0198fc277818 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Mon, 8 Apr 2019 09:46:05 +0100 Subject: [PATCH 17/31] Simplifying code for checking email domains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since this code isn’t trying to inherit from the code that also looked up domain names in `domains.yml` it can go back to being a lot simpler. This code is thoroughly tested already here: https://github.com/alphagov/notifications-admin/blob/a249382e6990635ddef9fef7fe614c9c8e57d55e/tests/app/main/test_validators.py#L74-L155 --- app/utils.py | 56 +++++++++++----------------------------------------- 1 file changed, 12 insertions(+), 44 deletions(-) diff --git a/app/utils.py b/app/utils.py index de4ac69d8..175ce6318 100644 --- a/app/utils.py +++ b/app/utils.py @@ -36,6 +36,11 @@ FAILURE_STATUSES = ['failed', 'temporary-failure', 'permanent-failure', 'technical-failure', 'virus-scan-failed', 'validation-failed'] REQUESTED_STATUSES = SENDING_STATUSES + DELIVERED_STATUSES + FAILURE_STATUSES +with open('{}/email_domains.yml'.format( + os.path.dirname(os.path.realpath(__file__)) +)) as email_domains: + GOVERNMENT_EMAIL_DOMAIN_NAMES = yaml.safe_load(email_domains) + def user_has_permissions(*permissions, **permission_kwargs): def wrap(func): @@ -280,11 +285,13 @@ def get_help_argument(): def is_gov_user(email_address): - try: - GovernmentEmailDomain(email_address) - return True - except NotGovernmentEmailDomain: - return False + return any( + email_address.lower().endswith(( + "@{}".format(known), + ".{}".format(known), + )) + for known in GOVERNMENT_EMAIL_DOMAIN_NAMES + ) def get_template( @@ -398,45 +405,6 @@ def set_status_filters(filter_args): ))) -_dir_path = os.path.dirname(os.path.realpath(__file__)) - - -class NotGovernmentEmailDomain(Exception): - pass - - -class GovernmentEmailDomain(): - - with open('{}/email_domains.yml'.format(_dir_path)) as email_domains: - domain_names = yaml.safe_load(email_domains) - - def __init__(self, email_address_or_domain): - try: - self._match = next(filter( - self.get_matching_function(email_address_or_domain), - self.domain_names, - )) - except StopIteration: - raise NotGovernmentEmailDomain() - - @staticmethod - def get_matching_function(email_address_or_domain): - - email_address_or_domain = email_address_or_domain.lower() - - def fn(domain): - - return ( - email_address_or_domain == domain - ) or ( - email_address_or_domain.endswith("@{}".format(domain)) - ) or ( - email_address_or_domain.endswith(".{}".format(domain)) - ) - - return fn - - def unicode_truncate(s, length): encoded = s.encode('utf-8')[:length] return encoded.decode('utf-8', 'ignore') From c7f3b688cd633bbfec2849791d2dedbd1416c678 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Mon, 1 Apr 2019 09:15:09 +0100 Subject: [PATCH 18/31] Bump version of NodeJS to 10.15.3 Updates the README to reference this. If running this locally, it's recommended to use the 'n' NPM package to manage your version of NodeJS, as described in the README: https://github.com/alphagov/notifications-admin#first-time-setup --- README.md | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 74ce65197..c9eeb68ae 100644 --- a/README.md +++ b/README.md @@ -22,19 +22,19 @@ Brew is a package manager for OSX. The following command installs brew: Languages needed - Python 3.4 -- [Node](https://nodejs.org/) 5.0.0 or greater -- [npm](https://www.npmjs.com/) 3.0.0 or greater +- [Node](https://nodejs.org/) 10.15.3 or greater +- [npm](https://www.npmjs.com/) 6.4.1 or greater ```shell brew install node ``` [NPM](npmjs.org) is Node's package management tool. `n` is a tool for managing -different versions of Node. The following installs `n` and uses the latest +different versions of Node. The following installs `n` and uses the long term support (LTS) version of Node. ```shell npm install -g n - n latest + n lts npm rebuild node-sass ``` diff --git a/package.json b/package.json index 3e81dc120..ee398e34e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Admin front end for GOV.UK Notify", "engines": { - "node": "5.0.0" + "node": "10.15.3" }, "scripts": { "test": "gulp lint", From c3a1e6ed59e011791439938cfea2858ec751b9ed Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Mon, 1 Apr 2019 09:58:13 +0100 Subject: [PATCH 19/31] Bump Gulp to 4.0.0 Brings in a new API which is a breaking change: https://github.com/gulpjs/gulp/blob/master/CHANGELOG.md#400 Requires gulpfile.babel.js to be a CommonJS module and for all tasks to be exported. Replaces using ES6 modules for the CommonJS to include other packages to match the type of module the gulpfile now is. Updates to 4.0.0 include `series` and `parallel` methods which remove the need for the `run-sequence` package. Also bumps gulp-specific libraries. --- gulpfile.babel.js | 301 +++++++++++++++++++++++----------------------- package.json | 13 +- 2 files changed, 158 insertions(+), 156 deletions(-) diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 1e68636df..69567cbd7 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -5,178 +5,181 @@ // 1. LIBRARIES // - - - - - - - - - - - - - - - -import gulp from 'gulp'; -import loadPlugins from 'gulp-load-plugins'; -import stylish from 'jshint-stylish'; -import runSequence from 'run-sequence'; +const { src, pipe, dest, series, parallel } = require('gulp'); +const loadPlugins = require('gulp-load-plugins'); +const stylish = require('jshint-stylish'); -const plugins = loadPlugins(), +const plugins = loadPlugins(); // 2. CONFIGURATION // - - - - - - - - - - - - - - - - paths = { - src: 'app/assets/', - dist: 'app/static/', - templates: 'app/templates/', - npm: 'node_modules/', - template: 'node_modules/govuk_template_jinja/', - toolkit: 'node_modules/govuk_frontend_toolkit/' - }; +const paths = { + src: 'app/assets/', + dist: 'app/static/', + templates: 'app/templates/', + npm: 'node_modules/', + template: 'node_modules/govuk_template_jinja/', + toolkit: 'node_modules/govuk_frontend_toolkit/' +}; // 3. TASKS // - - - - - - - - - - - - - - - // Move GOV.UK template resources -gulp.task('copy:govuk_template:template', () => gulp.src(paths.template + 'views/layouts/govuk_template.html') - .pipe(gulp.dest(paths.templates)) -); +const copy = { + govuk_template: { + template: () => { + return src(paths.template + 'views/layouts/govuk_template.html') + .pipe(dest(paths.templates)); + }, + css: () => { + return src(paths.template + 'assets/stylesheets/**/*.css') + .pipe(plugins.sass({ + outputStyle: 'compressed' + })) + .on('error', plugins.sass.logError) + .pipe(plugins.cssUrlAdjuster({ + prependRelative: process.env.NOTIFY_ENVIRONMENT == 'development' ? '/static/' : '/', + })) + .pipe(dest(paths.dist + 'stylesheets/')); + }, + js: () => { + return src(paths.template + 'assets/javascripts/**/*.js') + .pipe(plugins.uglify()) + .pipe(dest(paths.dist + 'javascripts/')); + }, + images: () => { + return src(paths.template + 'assets/stylesheets/images/**/*') + .pipe(dest(paths.dist + 'images/')); + }, + fonts: () => { + return src(paths.template + 'assets/stylesheets/fonts/**/*') + .pipe(dest(paths.dist + 'fonts/')); + }, + error_page: () => { + return src(paths.src + 'error_pages/**/*') + .pipe(dest(paths.dist + 'error_pages/')) + } + } +}; -gulp.task('copy:govuk_template:css', () => gulp.src(paths.template + 'assets/stylesheets/**/*.css') - .pipe(plugins.sass({ - outputStyle: 'compressed' - })) - .on('error', plugins.sass.logError) - .pipe(plugins.cssUrlAdjuster({ - prependRelative: process.env.NOTIFY_ENVIRONMENT == 'development' ? '/static/' : '/', - })) - .pipe(gulp.dest(paths.dist + 'stylesheets/')) -); -gulp.task('copy:govuk_template:js', () => gulp.src(paths.template + 'assets/javascripts/**/*.js') - .pipe(plugins.uglify()) - .pipe(gulp.dest(paths.dist + 'javascripts/')) -); +const javascripts = () => { + return src([ + paths.toolkit + 'javascripts/govuk/modules.js', + paths.toolkit + 'javascripts/govuk/show-hide-content.js', + paths.src + 'javascripts/stick-to-window-when-scrolling.js', + paths.src + 'javascripts/detailsPolyfill.js', + paths.src + 'javascripts/apiKey.js', + paths.src + 'javascripts/autofocus.js', + paths.src + 'javascripts/highlightTags.js', + paths.src + 'javascripts/fileUpload.js', + paths.src + 'javascripts/expandCollapse.js', + paths.src + 'javascripts/radioSelect.js', + paths.src + 'javascripts/updateContent.js', + paths.src + 'javascripts/listEntry.js', + paths.src + 'javascripts/liveSearch.js', + paths.src + 'javascripts/errorTracking.js', + paths.src + 'javascripts/preventDuplicateFormSubmissions.js', + paths.src + 'javascripts/fullscreenTable.js', + paths.src + 'javascripts/previewPane.js', + paths.src + 'javascripts/colourPreview.js', + paths.src + 'javascripts/templateFolderForm.js', + paths.src + 'javascripts/main.js' + ]) + .pipe(plugins.prettyerror()) + .pipe(plugins.babel({ + presets: ['es2015'] + })) + .pipe(plugins.addSrc.prepend([ + 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/diffDOM.js', + paths.npm + 'timeago/jquery.timeago.js', + paths.npm + 'textarea-caret/index.js' + ])) + .pipe(plugins.uglify()) + .pipe(plugins.concat('all.js')) + .pipe(dest(paths.dist + 'javascripts/')) +}; -gulp.task('copy:govuk_template:images', () => gulp.src(paths.template + 'assets/stylesheets/images/**/*') - .pipe(gulp.dest(paths.dist + 'images/')) -); -gulp.task('copy:govuk_template:fonts', () => gulp.src(paths.template + 'assets/stylesheets/fonts/**/*') - .pipe(gulp.dest(paths.dist + 'fonts/')) -); - -gulp.task('javascripts', () => gulp - .src([ - paths.toolkit + 'javascripts/govuk/modules.js', - paths.toolkit + 'javascripts/govuk/show-hide-content.js', - paths.src + 'javascripts/stick-to-window-when-scrolling.js', - paths.src + 'javascripts/detailsPolyfill.js', - paths.src + 'javascripts/apiKey.js', - paths.src + 'javascripts/autofocus.js', - paths.src + 'javascripts/highlightTags.js', - paths.src + 'javascripts/fileUpload.js', - paths.src + 'javascripts/expandCollapse.js', - paths.src + 'javascripts/radioSelect.js', - paths.src + 'javascripts/updateContent.js', - paths.src + 'javascripts/listEntry.js', - paths.src + 'javascripts/liveSearch.js', - paths.src + 'javascripts/errorTracking.js', - paths.src + 'javascripts/preventDuplicateFormSubmissions.js', - paths.src + 'javascripts/fullscreenTable.js', - paths.src + 'javascripts/previewPane.js', - paths.src + 'javascripts/colourPreview.js', - paths.src + 'javascripts/templateFolderForm.js', - paths.src + 'javascripts/main.js' - ]) - .pipe(plugins.prettyerror()) - .pipe(plugins.babel({ - presets: ['es2015'] - })) - .pipe(plugins.addSrc.prepend([ - 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/diffDOM.js', - paths.npm + 'timeago/jquery.timeago.js', - paths.npm + 'textarea-caret/index.js' - ])) - .pipe(plugins.uglify()) - .pipe(plugins.concat('all.js')) - .pipe(gulp.dest(paths.dist + 'javascripts/')) -); - -gulp.task('sass', () => gulp - .src(paths.src + '/stylesheets/main*.scss') - .pipe(plugins.prettyerror()) - .pipe(plugins.sass({ - outputStyle: 'compressed', - includePaths: [ - paths.npm + 'govuk-elements-sass/public/sass/', - paths.toolkit + 'stylesheets/' - ] - })) - .pipe(plugins.base64({baseDir: 'app'})) - .pipe(gulp.dest(paths.dist + 'stylesheets/')) -); +const sass = () => { + return src(paths.src + '/stylesheets/main*.scss') + .pipe(plugins.prettyerror()) + .pipe(plugins.sass({ + outputStyle: 'compressed', + includePaths: [ + paths.npm + 'govuk-elements-sass/public/sass/', + paths.toolkit + 'stylesheets/' + ] + })) + .pipe(plugins.base64({baseDir: 'app'})) + .pipe(dest(paths.dist + 'stylesheets/')) +}; // Copy images -gulp.task('images', () => gulp - .src([ - paths.src + 'images/**/*', - paths.toolkit + 'images/**/*', - paths.template + 'assets/images/**/*' - ]) - .pipe(gulp.dest(paths.dist + 'images/')) -); - -gulp.task('copy:govuk_template:error_page', () => gulp.src(paths.src + 'error_pages/**/*') - .pipe(gulp.dest(paths.dist + 'error_pages/')) -); +const images = () => { + return src([ + paths.src + 'images/**/*', + paths.toolkit + 'images/**/*', + paths.template + 'assets/images/**/*' + ]) + .pipe(dest(paths.dist + 'images/')) +}; // Watch for changes and re-run tasks -gulp.task('watchForChanges', function() { - gulp.watch(paths.src + 'javascripts/**/*', ['javascripts']); - gulp.watch(paths.src + 'stylesheets/**/*', ['sass']); - gulp.watch(paths.src + 'images/**/*', ['images']); - gulp.watch('gulpfile.babel.js', ['default']); -}); +const watchForChanges = () => { + return watch(paths.src + 'javascripts/**/*', ['javascripts']) + .watch(paths.src + 'stylesheets/**/*', ['sass']) + .watch(paths.src + 'images/**/*', ['images']) + .watch('gulpfile.babel.js', ['default']); +}; -gulp.task('lint:sass', () => gulp - .src([ - paths.src + 'stylesheets/*.scss', - paths.src + 'stylesheets/components/*.scss', - paths.src + 'stylesheets/views/*.scss', - ]) - .pipe(plugins.sassLint()) - .pipe(plugins.sassLint.format(stylish)) - .pipe(plugins.sassLint.failOnError()) -); - -gulp.task('lint:js', () => gulp - .src(paths.src + 'javascripts/**/*.js') - .pipe(plugins.jshint()) - .pipe(plugins.jshint.reporter(stylish)) - .pipe(plugins.jshint.reporter('fail')) -); - -gulp.task('lint', - ['lint:sass', 'lint:js'] -); +const lint = { + 'sass': () => { + return src([ + paths.src + 'stylesheets/*.scss', + paths.src + 'stylesheets/components/*.scss', + paths.src + 'stylesheets/views/*.scss', + ]) + .pipe(plugins.sassLint()) + .pipe(plugins.sassLint.format(stylish)) + .pipe(plugins.sassLint.failOnError()); + }, + 'js': (cb) => { + return src(paths.src + 'javascripts/**/*.js') + .pipe(plugins.jshint()) + .pipe(plugins.jshint.reporter(stylish)) + .pipe(plugins.jshint.reporter('fail')) + } +}; // Default: compile everything -gulp.task('default', function() { - runSequence( - [ - 'copy:govuk_template:template', - 'copy:govuk_template:images', - 'copy:govuk_template:fonts', - 'copy:govuk_template:css', - 'copy:govuk_template:js', - 'images', - ], - [ - 'copy:govuk_template:error_page', - 'javascripts', - 'sass' - ] - ); -}); +const defaultTask = parallel( + series( + copy.govuk_template.template, + copy.govuk_template.images, + copy.govuk_template.fonts, + copy.govuk_template.css, + copy.govuk_template.js, + images + ), + series( + copy.govuk_template.error_page, + javascripts, + sass + ) +); + +exports.default = defaultTask; + +exports.lint = series(lint.sass, lint.js); // Optional: recompile on changes -gulp.task('watch', - ['default', 'watchForChanges'] -); +exports.watch = series(defaultTask, watchForChanges); diff --git a/package.json b/package.json index ee398e34e..7397f0486 100644 --- a/package.json +++ b/package.json @@ -24,28 +24,27 @@ "govuk-elements-sass": "3.1.2", "govuk_frontend_toolkit": "7.2.0", "govuk_template_jinja": "0.24.0", - "gulp": "3.9.1", + "gulp": "4.0.0", "gulp-add-src": "1.0.0", "gulp-babel": "7.0.0", "gulp-base64": "0.1.3", "gulp-concat": "2.6.1", "gulp-include": "2.3.1", "gulp-load-plugins": "1.5.0", - "gulp-sass": "3.1.0", - "gulp-uglify": "3.0.0", + "gulp-sass": "4.0.2", + "gulp-uglify": "3.0.2", "hogan": "1.0.2", "jquery": "1.12.4", "query-command-supported": "1.0.0", - "textarea-caret": "^3.1.0", + "textarea-caret": "3.1.0", "timeago": "1.6.1" }, "devDependencies": { "gulp-css-url-adjuster": "0.2.3", "gulp-jshint": "2.1.0", "gulp-prettyerror": "1.2.1", - "gulp-sass-lint": "1.2.0", + "gulp-sass-lint": "1.4.0", "jshint": "2.9.5", - "jshint-stylish": "2.2.1", - "run-sequence": "2.2.1" + "jshint-stylish": "2.2.1" } } From b83053ddd1ec167a69ed9302588db15f796f8e9d Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Thu, 4 Apr 2019 14:44:46 +0100 Subject: [PATCH 20/31] Fix sass-lint Sass-lint was bumped when gulp-sass-lint was bumped. This brought in a new version which changed the interface of `sassLint.format`. It doesn't look like the `sassLint.format` method ever defined any arguments or did anything with the `arguments` variable. That being the case, our sending it an instance of the 'stylish' ESLint reporter did nothing in the previous version but this wasn't obvious because sass-lint defaults to 'stylish' for its output styling. The latest version (1.4.0) introduced an argument of a writable stream which, if defined, will be used to write the output to. This caused a problem with our sending in the instance of 'stylish'. This moves the selection of stylish into the sass-lint config, as described in the [sass-lint docs](https://github.com/sasstools/gulp-sass-lint/tree/master#sasslintformatwritable). --- gulpfile.babel.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 69567cbd7..c48c87985 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -148,8 +148,10 @@ const lint = { paths.src + 'stylesheets/components/*.scss', paths.src + 'stylesheets/views/*.scss', ]) - .pipe(plugins.sassLint()) - .pipe(plugins.sassLint.format(stylish)) + .pipe(plugins.sassLint({ + 'options': { 'formatter': 'stylish' } + })) + .pipe(plugins.sassLint.format()) .pipe(plugins.sassLint.failOnError()); }, 'js': (cb) => { From 71463182f17358aea895059a425df6478947438a Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Mon, 1 Apr 2019 10:06:50 +0100 Subject: [PATCH 21/31] Stop using Babel to transpile gulpfile Bumping NodeJS to 10.15.3 removes the need to transpile `gulpfile.js` as this version includes all the ES6 features used.. Also removes .babelrc file. This was included to ensure the gulpfile was transpiled correctly so it is no longer needed. --- .babelrc | 3 --- gulpfile.babel.js => gulpfile.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 .babelrc rename gulpfile.babel.js => gulpfile.js (99%) diff --git a/.babelrc b/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/gulpfile.babel.js b/gulpfile.js similarity index 99% rename from gulpfile.babel.js rename to gulpfile.js index c48c87985..4539934db 100644 --- a/gulpfile.babel.js +++ b/gulpfile.js @@ -138,7 +138,7 @@ const watchForChanges = () => { return watch(paths.src + 'javascripts/**/*', ['javascripts']) .watch(paths.src + 'stylesheets/**/*', ['sass']) .watch(paths.src + 'images/**/*', ['images']) - .watch('gulpfile.babel.js', ['default']); + .watch('gulpfile.js', ['default']); }; const lint = { From fed3381209f7b6e5dc403a47007f2f59e188515b Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Mon, 1 Apr 2019 12:09:38 +0100 Subject: [PATCH 22/31] Bump Babel to 7.4.0 Requires replacing `babel-preset-es2015` for `babel-preset-env` as Babel switched to this as of version 6: https://babeljs.io/docs/en/babel-preset-es2015 Note: this also moves all Babel packages to using the scoped packages syntax, descended from the `@babel` namespace. https://docs.npmjs.com/about-scopes --- gulpfile.js | 2 +- package.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 4539934db..7009c9a93 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -90,7 +90,7 @@ const javascripts = () => { ]) .pipe(plugins.prettyerror()) .pipe(plugins.babel({ - presets: ['es2015'] + presets: ['@babel/preset-env'] })) .pipe(plugins.addSrc.prepend([ paths.npm + 'hogan.js/dist/hogan-3.0.2.js', diff --git a/package.json b/package.json index 7397f0486..c7f9ba78e 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,15 @@ "license": "MIT", "homepage": "https://github.com/alphagov/notifications-admin#readme", "dependencies": { - "babel-core": "6.26.0", - "babel-preset-es2015": "6.24.1", + "@babel/core": "7.4.0", + "@babel/preset-env": "7.4.2", "diff-dom": "2.3.1", "govuk-elements-sass": "3.1.2", "govuk_frontend_toolkit": "7.2.0", "govuk_template_jinja": "0.24.0", "gulp": "4.0.0", "gulp-add-src": "1.0.0", - "gulp-babel": "7.0.0", + "gulp-babel": "8.0.0", "gulp-base64": "0.1.3", "gulp-concat": "2.6.1", "gulp-include": "2.3.1", From 05f3cb6797a0c6dfe439533a593b3a8059cdbe54 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Mon, 1 Apr 2019 16:41:23 +0100 Subject: [PATCH 23/31] Bump JSHint --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7f9ba78e..26ec1bedb 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "gulp-jshint": "2.1.0", "gulp-prettyerror": "1.2.1", "gulp-sass-lint": "1.4.0", - "jshint": "2.9.5", + "jshint": "2.10.2", "jshint-stylish": "2.2.1" } } From 01b97986f6b24c956d20e1b4b6db8445b75c157f Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Wed, 3 Apr 2019 16:02:23 +0100 Subject: [PATCH 24/31] Update diff-dom package to 3.1.0 Since it moved to ES Modules in version 2.3.1, diff-dom stopped including the `diffDOM.js` file in its NPM package. We don't do any kind of bundling in our build yet, just concatenation of our scripts and some minification of the results so we can't take advantage of this yet. The `diffDOM.js` file is still available in the Github release so this moves to referencing that in the `package.json` instead, until we start using a bundler. I opened an issue to check this is what the author intended: https://github.com/fiduswriter/diffDOM/issues/84 The latest version also adds Rollup as a peer dependency. --- app/assets/javascripts/updateContent.js | 2 +- gulpfile.js | 2 +- package.json | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/updateContent.js b/app/assets/javascripts/updateContent.js index 516bd913f..eb94f33eb 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(); + var dd = new diffDOM.DiffDOM(); var getRenderer = $component => response => dd.apply( $component.get(0), diff --git a/gulpfile.js b/gulpfile.js index 7009c9a93..7f3347b79 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -96,7 +96,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/diffDOM.js', + paths.npm + 'diff-dom/browser/diffDOM.js', paths.npm + 'timeago/jquery.timeago.js', paths.npm + 'textarea-caret/index.js' ])) diff --git a/package.json b/package.json index 26ec1bedb..0105d04da 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": "2.3.1", + "diff-dom": "https://github.com/fiduswriter/diffDOM/archive/v3.1.0.tar.gz", "govuk-elements-sass": "3.1.2", "govuk_frontend_toolkit": "7.2.0", "govuk_template_jinja": "0.24.0", @@ -46,5 +46,8 @@ "gulp-sass-lint": "1.4.0", "jshint": "2.10.2", "jshint-stylish": "2.2.1" + }, + "peerDependencies": { + "rollup": "1.10.0" } } From fa851dcfecc3c148d097851c0b5a919529c7c005 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Mon, 1 Apr 2019 14:57:47 +0100 Subject: [PATCH 25/31] Bump remaining NPM packages used on client-side Means that only the following packages will now be out of date: 1. govuk_template_jinja 2. jquery Reasons: GOVUK Template Jinja This bumps it to 0.24.1, after which the only changes are to add analytics tracking to the GOV.UK logo and cookie banner. We don't need to track either of these and they were added for GOVUK analytics work. jQuery jQuery stops supporting IE<9 after version 1.12. This is pretty much our support now but if we move to IE>8 we shouldn't do it by bumping the version. This would just increase the amount of broken JS we ship to IE8. Also: both are deprecated and we will move off them in the near future. --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 0105d04da..f596c195e 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,9 @@ "@babel/core": "7.4.0", "@babel/preset-env": "7.4.2", "diff-dom": "https://github.com/fiduswriter/diffDOM/archive/v3.1.0.tar.gz", - "govuk-elements-sass": "3.1.2", - "govuk_frontend_toolkit": "7.2.0", - "govuk_template_jinja": "0.24.0", + "govuk-elements-sass": "3.1.3", + "govuk_frontend_toolkit": "8.1.0", + "govuk_template_jinja": "0.24.1", "gulp": "4.0.0", "gulp-add-src": "1.0.0", "gulp-babel": "8.0.0", @@ -37,7 +37,7 @@ "jquery": "1.12.4", "query-command-supported": "1.0.0", "textarea-caret": "3.1.0", - "timeago": "1.6.1" + "timeago": "1.6.5" }, "devDependencies": { "gulp-css-url-adjuster": "0.2.3", From a8a29698cb376cc747586c829cb434234a17cc4d Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Fri, 5 Apr 2019 13:16:12 +0100 Subject: [PATCH 26/31] Replace gulp-base64 with gulp-base64-inline The gulp-base64 package has 11 dependencies with vulnerabilities listed against them as of this time. It also doesn't seem to be maintained any more. The last commit was in 2015 and there are issues and pull requests up to bump the dependencies. This replaces it with gulp-base64-inline. gulp-base64-inline takes a single path, which it prepends to any image paths it finds. Our image paths are actually URLs, not filesystem paths so we need to send it a relative path to repoint the URL. This commit includes changes that remove a few `@import`s from one of our sass partials. They aren't needed as those files are imported further up the stack and `_typography.scss` has an import in it that overwrites the new `_url-helpers.scss` we added here. --- app/assets/stylesheets/_url-helpers.scss | 14 ++++++++++++++ .../vendor/previous-next-navigation.scss | 2 -- app/assets/stylesheets/main.scss | 4 +++- gulpfile.js | 6 ++++-- package.json | 2 +- 5 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 app/assets/stylesheets/_url-helpers.scss diff --git a/app/assets/stylesheets/_url-helpers.scss b/app/assets/stylesheets/_url-helpers.scss new file mode 100644 index 000000000..9643d295a --- /dev/null +++ b/app/assets/stylesheets/_url-helpers.scss @@ -0,0 +1,14 @@ +// Copy of _url-helpers.scss in govuk_frontend_toolkit +// to allow us to use gulp-base64-inline on all our images +// gulp-base64-inline requires you to specify which of your +// images you want encoded by using the `inline()` function +// see https://github.com/goschevski/gulp-base64-inline +@function file-url($file) { + $url: ''; + @if $path { + $url: inline($path + $file); + } @else { + $url: image-url($file); + } + @return $url; +} diff --git a/app/assets/stylesheets/components/vendor/previous-next-navigation.scss b/app/assets/stylesheets/components/vendor/previous-next-navigation.scss index 5f8f9f4d9..0e060f12c 100644 --- a/app/assets/stylesheets/components/vendor/previous-next-navigation.scss +++ b/app/assets/stylesheets/components/vendor/previous-next-navigation.scss @@ -4,8 +4,6 @@ https://github.com/alphagov/static/blob/3d93a762b9d7af54615c77ae3e479131c03b8175 and https://github.com/alphagov/static/blob/da8aeeaa749093eab30286d7fc9f965533b66f47/app/assets/stylesheets/styleguide/_conditionals2.scss */ -@import "_colours.scss"; -@import "_typography.scss"; // Media query helpers. These make producing IE layouts // super easy. diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index a12340dca..8799cbf73 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -11,9 +11,11 @@ $path: '/static/images/'; @import 'typography'; @import 'grid_layout'; @import 'helpers'; -@import 'url-helpers'; @import 'design-patterns/buttons'; +// Dependencies from GOVU.UK Frontend Toolkit, rewritten for this application +@import 'url-helpers'; + // Dependencies from GOV.UK Elements // https://github.com/alphagov/govuk_elements @import 'elements/helpers'; diff --git a/gulpfile.js b/gulpfile.js index 7f3347b79..9e09de08d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,7 +9,9 @@ const { src, pipe, dest, series, parallel } = require('gulp'); const loadPlugins = require('gulp-load-plugins'); const stylish = require('jshint-stylish'); -const plugins = loadPlugins(); +const plugins = loadPlugins({ + 'rename': { 'gulp-base64-inline': 'base64' } +}); // 2. CONFIGURATION // - - - - - - - - - - - - - - - @@ -116,7 +118,7 @@ const sass = () => { paths.toolkit + 'stylesheets/' ] })) - .pipe(plugins.base64({baseDir: 'app'})) + .pipe(plugins.base64('../..')) .pipe(dest(paths.dist + 'stylesheets/')) }; diff --git a/package.json b/package.json index f596c195e..640989964 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "gulp": "4.0.0", "gulp-add-src": "1.0.0", "gulp-babel": "8.0.0", - "gulp-base64": "0.1.3", + "gulp-base64-inline": "1.0.4", "gulp-concat": "2.6.1", "gulp-include": "2.3.1", "gulp-load-plugins": "1.5.0", From 4766390f178555cc859f3b664ce7ea5fa3a50c4d Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Fri, 5 Apr 2019 13:31:13 +0100 Subject: [PATCH 27/31] Remove gulp-load-plugins It's not being updated at the moment and has a few vulnerabilities listed against its dependencies. --- gulpfile.js | 15 +++++++++++---- package.json | 1 - 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 9e09de08d..60533475f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,12 +6,19 @@ // 1. LIBRARIES // - - - - - - - - - - - - - - - const { src, pipe, dest, series, parallel } = require('gulp'); -const loadPlugins = require('gulp-load-plugins'); const stylish = require('jshint-stylish'); -const plugins = loadPlugins({ - 'rename': { 'gulp-base64-inline': 'base64' } -}); +const plugins = {}; +plugins.addSrc = require('gulp-add-src'); +plugins.babel = require('gulp-babel'); +plugins.base64 = require('gulp-base64-inline'); +plugins.concat = require('gulp-concat'); +plugins.cssUrlAdjuster = require('gulp-css-url-adjuster'); +plugins.jshint = require('gulp-jshint'); +plugins.prettyerror = require('gulp-prettyerror'); +plugins.sass = require('gulp-sass'); +plugins.sassLint = require('gulp-sass-lint'); +plugins.uglify = require('gulp-uglify'); // 2. CONFIGURATION // - - - - - - - - - - - - - - - diff --git a/package.json b/package.json index 640989964..3306a0f6b 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "gulp-base64-inline": "1.0.4", "gulp-concat": "2.6.1", "gulp-include": "2.3.1", - "gulp-load-plugins": "1.5.0", "gulp-sass": "4.0.2", "gulp-uglify": "3.0.2", "hogan": "1.0.2", From a4bca73b80a395b1fd67e1e595643545b1e03e13 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Thu, 4 Apr 2019 14:30:06 +0100 Subject: [PATCH 28/31] Make Jenkins use Node 10 --- docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 4c41b9185..c9464a30b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,7 +6,7 @@ ARG NO_PROXY ENV PYTHONUNBUFFERED=1 \ DEBIAN_FRONTEND=noninteractive \ - NODEJS_VERSION=6.3.1-1nodesource1~jessie1 \ + NODEJS_VERSION=10.x \ GOSU_VERSION=1.10 RUN \ @@ -42,8 +42,8 @@ RUN \ && echo "Install nodejs" \ && cd /tmp \ - && curl -x "$HTTP_PROXY" -sSLO https://deb.nodesource.com/node_6.x/pool/main/n/nodejs/nodejs_${NODEJS_VERSION}_amd64.deb \ - && dpkg -i /tmp/nodejs_${NODEJS_VERSION}_amd64.deb \ + && curl -x "$HTTP_PROXY" -sL https://deb.nodesource.com/setup_${NODEJS_VERSION} | bash - \ + && apt-get install -y --no-install-recommends nodejs \ && echo "Clean up" \ && rm -rf /var/lib/apt/lists/* /tmp/* From 700e073d0f49cf7e39b0f9735d5faaa4001265fa Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Fri, 12 Apr 2019 17:00:11 +0100 Subject: [PATCH 29/31] Stop sending service domain to the API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API doesn’t look at it, and the rest of the admin code isn’t calling the method with this argument any more. --- app/notify_client/service_api_client.py | 2 -- tests/app/notify_client/test_service_api_client.py | 2 -- tests/app/notify_client/test_user_client.py | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index b79651dbd..86cbaaa58 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -13,7 +13,6 @@ class ServiceAPIClient(NotifyAdminAPIClient): restricted, user_id, email_from, - service_domain, ): """ Create a service and return the json. @@ -26,7 +25,6 @@ class ServiceAPIClient(NotifyAdminAPIClient): "user_id": user_id, "restricted": restricted, "email_from": email_from, - "service_domain": service_domain } data = _attach_current_user(data) return self.post("/service", data)['data']['id'] diff --git a/tests/app/notify_client/test_service_api_client.py b/tests/app/notify_client/test_service_api_client.py index d3a34fd8c..24d350146 100644 --- a/tests/app/notify_client/test_service_api_client.py +++ b/tests/app/notify_client/test_service_api_client.py @@ -74,7 +74,6 @@ def test_client_creates_service_with_correct_data( True, fake_uuid, 'test@example.com', - 'nhs.uk' ) mock_post.assert_called_once_with( '/service', @@ -90,7 +89,6 @@ def test_client_creates_service_with_correct_data( restricted=True, user_id=fake_uuid, email_from='test@example.com', - service_domain='nhs.uk' ), ) diff --git a/tests/app/notify_client/test_user_client.py b/tests/app/notify_client/test_user_client.py index d5346fe4f..b7d41bf92 100644 --- a/tests/app/notify_client/test_user_client.py +++ b/tests/app/notify_client/test_user_client.py @@ -253,7 +253,7 @@ def test_returns_value_from_cache( (user_api_client, 'set_user_permissions', [user_id, SERVICE_ONE_ID, []], {}), (user_api_client, 'activate_user', [api_user_pending(sample_uuid())], {}), (service_api_client, 'remove_user_from_service', [SERVICE_ONE_ID, user_id], {}), - (service_api_client, 'create_service', ['', '', 0, False, user_id, sample_uuid(), ''], {}), + (service_api_client, 'create_service', ['', '', 0, False, user_id, sample_uuid()], {}), (invite_api_client, 'accept_invite', [SERVICE_ONE_ID, user_id], {}), ]) def test_deletes_user_cache( From 8626777fdeac5bb4f27a407e72e04e9226b882fd Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Tue, 9 Apr 2019 09:39:45 +0100 Subject: [PATCH 30/31] =?UTF-8?q?Don=E2=80=99t=20let=20services=20go=20liv?= =?UTF-8?q?e=20without=20an=20organisation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We shouldn’t be making services live without an agreement in place. In order to have an agreement in place we need to know what organisation operates the service. If a service comes along but belongs to an organisation we don’t know about then we should create that organisation. This commits removes the link, which should force this to happen. --- app/templates/views/service-settings.html | 9 ++++++-- tests/app/main/views/test_service_settings.py | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/templates/views/service-settings.html b/app/templates/views/service-settings.html index 0a18bde59..6b81af730 100644 --- a/app/templates/views/service-settings.html +++ b/app/templates/views/service-settings.html @@ -288,8 +288,13 @@ {% call row() %} {{ text_field('Live')}} - {{ boolean_field(not current_service.trial_mode) }} - {{ edit_field('Change', url_for('.service_switch_live', service_id=current_service.id)) }} + {% if current_service.trial_mode and not current_service.organisation %} + {{ text_field('No (you need to assign this service to an organisation before you can make it live)') }} + {{ text_field('') }} + {% else %} + {{ boolean_field(not current_service.trial_mode) }} + {{ edit_field('Change', url_for('.service_switch_live', service_id=current_service.id)) }} + {% endif %} {% endcall %} {% call row() %} diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 78d25e59c..83dd4d135 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -144,6 +144,29 @@ def test_should_show_overview( app.service_api_client.get_service.assert_called_with(service_one['id']) +def test_no_go_live_link_for_service_without_organisation( + client_request, + mocker, + no_reply_to_email_addresses, + no_letter_contact_blocks, + mock_get_service_organisation, + single_sms_sender, + platform_admin_user, + mock_get_service_settings_page_common, +): + mocker.patch('app.organisations_client.get_service_organisation', return_value=None) + client_request.login(platform_admin_user) + page = client_request.get('main.service_settings', service_id=SERVICE_ONE_ID) + + assert page.find('h1').text == 'Settings' + assert normalize_spaces(page.select('tr')[16].text) == ( + 'Live No (you need to assign this service to an organisation before you can make it live)' + ) + assert normalize_spaces(page.select('tr')[18].text) == ( + 'Organisation Not set Change' + ) + + @pytest.mark.parametrize('permissions, expected_rows', [ (['email', 'sms', 'inbound_sms', 'international_sms'], [ From b4cc4c9beae1cd83b92863d3705c4d4e26e24b4c Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Tue, 16 Apr 2019 11:10:25 +0100 Subject: [PATCH 31/31] Revert bump to library to unblock functional tests Version 3.1.3 of govuk-elements-sass makes everything with the `heading-small` class `display: block`. https://github.com/alphagov/govuk_elements/pull/552 We use that class in many places just to make the text bold with the assumption that it the styles it applies will not prevent our text rendering inline. This change breaks that assumption. We do need to swap these classes out for utility classes that just apply the bold styles but it can be done at a later date and we aren't missing out on much by reverting this library. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3306a0f6b..2273a1e9a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@babel/core": "7.4.0", "@babel/preset-env": "7.4.2", "diff-dom": "https://github.com/fiduswriter/diffDOM/archive/v3.1.0.tar.gz", - "govuk-elements-sass": "3.1.3", + "govuk-elements-sass": "3.1.2", "govuk_frontend_toolkit": "8.1.0", "govuk_template_jinja": "0.24.1", "gulp": "4.0.0",