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/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/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/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/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 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/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/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/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/index.py b/app/main/views/index.py index e7e758978..aa5d2bc3c 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -12,18 +12,23 @@ 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 -from app.utils import AgreementInfo, get_logo_cdn_domain +from app.utils import 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') @@ -70,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(), ) @@ -268,7 +272,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/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/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/main/views/service_settings.py b/app/main/views/service_settings.py index ba20ccbb1..20aa95c2c 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=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') @@ -262,12 +261,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( @@ -985,7 +979,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,37 +1067,6 @@ 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, agreement_signed): - - BASE = 'notify_request_to_go_live' - - yield BASE - - if service.go_live_checklist_completed and agreement_signed: - yield BASE + '_complete' - return - - for test, tag in ( - (True, ''), - (not service.volumes, '_volumes'), - (not service.go_live_checklist_completed, '_checklist'), - (not 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/__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..c9cdef32b --- /dev/null +++ b/app/models/organisation.py @@ -0,0 +1,134 @@ +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 + + 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.name: + 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), + ) + else: + return 'Can’t tell (domain is {})'.format(fallback_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): + if self.crown 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 is None: + abort(404) + return self.crown diff --git a/app/models/service.py b/app/models/service.py index 64805f430..dea52c3dc 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,28 +52,18 @@ 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) + 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, @@ -402,8 +394,10 @@ class Service(): return None @cached_property - def organisation_name(self): - return organisations_client.get_service_organisation(self.id).get('name', None) + def organisation(self): + return Organisation( + organisations_client.get_service_organisation(self.id) + ) @cached_property def inbound_number(self): @@ -552,3 +546,30 @@ class Service(): 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/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/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/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), diff --git a/app/notify_client/organisations_api_client.py b/app/notify_client/organisations_api_client.py index d1d082e82..7b525dc42 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 @@ -25,6 +37,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..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'] @@ -98,6 +96,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/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 @@
+ 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) %} ++ You must complete these steps before you can request to go live. +
+ {% 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/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 @@- {{ 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/app/utils.py b/app/utils.py index 2a70ab3fb..175ce6318 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 @@ -44,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): @@ -288,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( @@ -406,204 +405,6 @@ def set_status_filters(filter_args): ))) -_dir_path = os.path.dirname(os.path.realpath(__file__)) - - -class AgreementInfo: - - with open('{}/domains.yml'.format(_dir_path)) as domains: - domains = yaml.safe_load(domains) - domain_names = sorted(domains.keys(), key=len, reverse=True) - - 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 - - @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 _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] return encoded.decode('utf-8', 'ignore') 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/* diff --git a/gulpfile.babel.js b/gulpfile.babel.js deleted file mode 100644 index 1e68636df..000000000 --- a/gulpfile.babel.js +++ /dev/null @@ -1,182 +0,0 @@ -// GULPFILE -// - - - - - - - - - - - - - - - -// This file processes all of the assets in the "src" folder -// and outputs the finished files in the "dist" folder. - -// 1. LIBRARIES -// - - - - - - - - - - - - - - - -import gulp from 'gulp'; -import loadPlugins from 'gulp-load-plugins'; -import stylish from 'jshint-stylish'; -import runSequence from 'run-sequence'; - -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/' - }; - -// 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)) -); - -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/')) -); - -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/')) -); - - -// 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/')) -); - - -// 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']); -}); - -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'] -); - -// 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' - ] - ); -}); - -// Optional: recompile on changes -gulp.task('watch', - ['default', 'watchForChanges'] -); diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 000000000..60533475f --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,196 @@ +// GULPFILE +// - - - - - - - - - - - - - - - +// This file processes all of the assets in the "src" folder +// and outputs the finished files in the "dist" folder. + +// 1. LIBRARIES +// - - - - - - - - - - - - - - - +const { src, pipe, dest, series, parallel } = require('gulp'); +const stylish = require('jshint-stylish'); + +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 +// - - - - - - - - - - - - - - - +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 + +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/')) + } + } +}; + + +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: ['@babel/preset-env'] + })) + .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/browser/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/')) +}; + + +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('../..')) + .pipe(dest(paths.dist + 'stylesheets/')) +}; + + +// Copy images + +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 +const watchForChanges = () => { + return watch(paths.src + 'javascripts/**/*', ['javascripts']) + .watch(paths.src + 'stylesheets/**/*', ['sass']) + .watch(paths.src + 'images/**/*', ['images']) + .watch('gulpfile.js', ['default']); +}; + +const lint = { + 'sass': () => { + return src([ + paths.src + 'stylesheets/*.scss', + paths.src + 'stylesheets/components/*.scss', + paths.src + 'stylesheets/views/*.scss', + ]) + .pipe(plugins.sassLint({ + 'options': { 'formatter': 'stylish' } + })) + .pipe(plugins.sassLint.format()) + .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 +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 +exports.watch = series(defaultTask, watchForChanges); diff --git a/package.json b/package.json index 3e81dc120..2273a1e9a 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", @@ -18,34 +18,35 @@ "license": "MIT", "homepage": "https://github.com/alphagov/notifications-admin#readme", "dependencies": { - "babel-core": "6.26.0", - "babel-preset-es2015": "6.24.1", - "diff-dom": "2.3.1", + "@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", - "gulp": "3.9.1", + "govuk_frontend_toolkit": "8.1.0", + "govuk_template_jinja": "0.24.1", + "gulp": "4.0.0", "gulp-add-src": "1.0.0", - "gulp-babel": "7.0.0", - "gulp-base64": "0.1.3", + "gulp-babel": "8.0.0", + "gulp-base64-inline": "1.0.4", "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", - "timeago": "1.6.1" + "textarea-caret": "3.1.0", + "timeago": "1.6.5" }, "devDependencies": { "gulp-css-url-adjuster": "0.2.3", "gulp-jshint": "2.1.0", "gulp-prettyerror": "1.2.1", - "gulp-sass-lint": "1.2.0", - "jshint": "2.9.5", - "jshint-stylish": "2.2.1", - "run-sequence": "2.2.1" + "gulp-sass-lint": "1.4.0", + "jshint": "2.10.2", + "jshint-stylish": "2.2.1" + }, + "peerDependencies": { + "rollup": "1.10.0" } } diff --git a/requirements-app.txt b/requirements-app.txt index 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 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_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/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 == [] 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_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 3f90b1a20..3c0a5998b 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, ) @@ -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, @@ -69,6 +82,7 @@ def test_robots(client): ]) def test_static_pages( client_request, + mock_get_organisation_by_domain, view, ): page = client_request.get('main.{}'.format(view)) @@ -134,12 +148,14 @@ def test_terms_is_generic_if_user_is_not_logged_in( @pytest.mark.parametrize(( - 'email_address,' + 'name,' + 'agreement_signed,' 'expected_terms_paragraph,' 'expected_terms_link,' ), [ ( - 'test@cabinet-office.gov.uk', + 'Cabinet Office', + True, ( 'Your organisation (Cabinet Office) has already accepted ' 'the GOV.UK Notify data sharing and financial agreement.' @@ -147,7 +163,8 @@ def test_terms_is_generic_if_user_is_not_logged_in( None, ), ( - 'test@aylesburytowncouncil.gov.uk', + 'Aylesbury Town Council', + False, ( 'Your organisation (Aylesbury Town Council) must also ' 'accept our data sharing and financial agreement. Download ' @@ -159,7 +176,8 @@ def test_terms_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 ' @@ -172,7 +190,8 @@ def test_terms_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.' @@ -187,13 +206,16 @@ 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, ): - 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') assert normalize_spaces(terms_page.select('main p')[1].text) == expected_terms_paragraph @@ -205,7 +227,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/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/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, diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index dc81457af..83dd4d135 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -30,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, @@ -100,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', @@ -143,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'], [ @@ -615,6 +639,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, @@ -674,6 +699,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, @@ -684,7 +710,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)), @@ -731,18 +756,75 @@ 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, + mock_get_service_organisation, + single_sms_sender, + checklist_completed, + agreement_signed, + expected_button, +): + mocker.patch( + 'app.models.service.Service.go_live_checklist_completed', + new_callable=PropertyMock, + return_value=checklist_completed, + ) + mocker.patch( + 'app.models.organisation.Organisation.agreement_signed', + new_callable=PropertyMock, + return_value=agreement_signed, + create=True, + ) + + 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) == ( + 'You must complete these steps before you can request to go live.' + ) + + @pytest.mark.parametrize(( 'estimated_sms_volume,' 'organisation_type,' @@ -833,13 +915,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): @@ -887,25 +969,23 @@ 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) -@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', ), )) @@ -913,7 +993,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( @@ -941,9 +1021,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 @@ -958,6 +1039,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', @@ -1228,7 +1310,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, @@ -1239,6 +1320,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), @@ -1457,6 +1543,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', @@ -1487,10 +1578,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}), - agreement_signed, - )) == expected_tags + assert app.models.service.Service( + {'id': SERVICE_ONE_ID} + ).request_to_go_live_tags == expected_tags @pytest.mark.parametrize('route', [ @@ -3907,6 +3997,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, @@ -3915,10 +4009,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, @@ -3934,14 +4034,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/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/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( 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'), diff --git a/tests/conftest.py b/tests/conftest.py index b07b375da..6fa21eae0 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) @@ -2541,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) @@ -2561,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, } ] @@ -2589,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 @@ -2613,7 +2607,6 @@ def create_email_branding(id, non_standard_values={}): 'text': 'Organisation text', 'id': id, 'colour': '#f00', - 'domain': 'sample.com', 'brand_type': 'org', } @@ -2676,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( @@ -2686,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( @@ -3126,9 +3119,40 @@ def mock_get_organisation( @pytest.fixture(scope='function') -def mock_get_service_organisation(mocker): +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, + 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) @@ -3351,3 +3375,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, + })