Browse Source

Merge branch 'main' into cldc-1249-supported-housing-scheme

pull/705/head
Stéphane Meny 3 years ago committed by GitHub
parent
commit
cad8e407fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      Gemfile
  2. 41
      Gemfile.lock
  3. 270
      README.md
  4. 105
      app/controllers/schemes_controller.rb
  5. 8
      app/frontend/application.js
  6. 8
      app/frontend/controllers/govukfrontend_controller.js
  7. 134
      app/frontend/styles/_pagination.scss
  8. 1
      app/frontend/styles/application.scss
  9. 2
      app/helpers/details_table_helper.rb
  10. 4
      app/helpers/tab_nav_helper.rb
  11. 3
      app/models/organisation.rb
  12. 182
      app/models/scheme.rb
  13. 8
      app/models/validations/financial_validations.rb
  14. 2
      app/views/layouts/_footer.html.erb
  15. 4
      app/views/organisations/schemes.html.erb
  16. 39
      app/views/pagy/_nav.html.erb
  17. 2
      app/views/schemes/_scheme_list.html.erb
  18. 65
      app/views/schemes/check_answers.html.erb
  19. 32
      app/views/schemes/confirm_secondary.html.erb
  20. 76
      app/views/schemes/details.html.erb
  21. 4
      app/views/schemes/index.html.erb
  22. 72
      app/views/schemes/new.html.erb
  23. 35
      app/views/schemes/primary_client_group.html.erb
  24. 35
      app/views/schemes/secondary_client_group.html.erb
  25. 39
      app/views/schemes/support.html.erb
  26. 2
      app/views/start/index.html.erb
  27. 10
      config/locales/en.yml
  28. 9
      config/routes.rb
  29. 2
      db/migrate/20220616130451_add_reference_to_case_log.rb
  30. 7
      db/migrate/20220623132228_add_has_other_client_group_field.rb
  31. 5
      db/migrate/20220629100324_remove_code_from_schemes.rb
  32. 5
      db/migrate/20220629105452_add_stock_owning_organisation_to_schemes.rb
  33. 5
      db/schema.rb
  34. 3
      db/seeds.rb
  35. 195
      docs/developer_setup.md
  36. 17
      docs/exports.md
  37. 190
      docs/form_builder.md
  38. 21
      docs/form_runner.md
  39. 49
      docs/frontend.md
  40. BIN
      docs/images/architecture.png
  41. BIN
      docs/images/organisational_relationships.png
  42. BIN
      docs/images/service.png
  43. BIN
      docs/images/user_log_permissions.png
  44. 4
      docs/index.html
  45. 148
      docs/infrastructure.md
  46. 17
      docs/monitoring.md
  47. 25
      docs/organisations.md
  48. 9
      docs/schemes.md
  49. 5
      docs/service_overview.md
  50. 13
      docs/testing.md
  51. 15
      docs/users.md
  52. 56
      infrastructure_setup.md
  53. 4
      package.json
  54. 1
      spec/components/search_component_spec.rb
  55. 1
      spec/components/search_result_caption_component_spec.rb
  56. 2
      spec/factories/location.rb
  57. 1
      spec/factories/scheme.rb
  58. 408
      spec/features/schemes_spec.rb
  59. 4
      spec/helpers/tab_nav_helper_spec.rb
  60. 10
      spec/models/organisation_spec.rb
  61. 41
      spec/models/scheme_spec.rb
  62. 58
      spec/models/validations/financial_validations_spec.rb
  63. 4
      spec/requests/case_logs_controller_spec.rb
  64. 30
      spec/requests/organisations_controller_spec.rb
  65. 1041
      spec/requests/schemes_controller_spec.rb
  66. 2094
      yarn.lock

3
Gemfile

@ -55,8 +55,7 @@ gem "redis"
# Receive exceptions and configure alerts # Receive exceptions and configure alerts
gem "sentry-rails" gem "sentry-rails"
gem "sentry-ruby" gem "sentry-ruby"
# Pagination # Possessives in strings
gem "pagy"
gem "possessive" gem "possessive"
group :development, :test do group :development, :test do

41
Gemfile.lock

@ -93,8 +93,8 @@ GEM
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.598.0) aws-partitions (1.601.0)
aws-sdk-core (3.131.1) aws-sdk-core (3.131.2)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0) aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
@ -153,7 +153,7 @@ GEM
dotenv (= 2.7.6) dotenv (= 2.7.6)
railties (>= 3.2) railties (>= 3.2)
encryptor (3.0.0) encryptor (3.0.0)
erb_lint (0.1.1) erb_lint (0.1.3)
activesupport activesupport
better_html (~> 1.0.7) better_html (~> 1.0.7)
html_tokenizer html_tokenizer
@ -173,28 +173,30 @@ GEM
ffi (1.15.5) ffi (1.15.5)
globalid (1.0.0) globalid (1.0.0)
activesupport (>= 5.0) activesupport (>= 5.0)
govuk-components (3.0.6) govuk-components (3.1.2)
actionpack (>= 6.1)
activemodel (>= 6.1) activemodel (>= 6.1)
html-attributes-utils (~> 0.9.0) html-attributes-utils (~> 0.9, >= 0.9.2)
pagy (~> 5.10.1)
railties (>= 6.1) railties (>= 6.1)
view_component (~> 2.49.1) view_component (~> 2.56.2)
govuk_design_system_formbuilder (3.0.3) govuk_design_system_formbuilder (3.1.0)
actionview (>= 6.1) actionview (>= 6.1)
activemodel (>= 6.1) activemodel (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
html-attributes-utils (~> 0.9.0) html-attributes-utils (~> 0.9, >= 0.9.2)
govuk_markdown (1.0.0) govuk_markdown (1.0.0)
activesupport activesupport
redcarpet redcarpet
hashdiff (1.0.1) hashdiff (1.0.1)
html-attributes-utils (0.9.0) html-attributes-utils (0.9.2)
activesupport (>= 6.1.4.4) activesupport (>= 6.1.4.4)
html_tokenizer (0.0.7) html_tokenizer (0.0.7)
i18n (1.10.0) i18n (1.10.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
iniparse (1.5.0) iniparse (1.5.0)
jmespath (1.6.1) jmespath (1.6.1)
jsbundling-rails (1.0.2) jsbundling-rails (1.0.3)
railties (>= 6.0.0) railties (>= 6.0.0)
json-schema (3.0.0) json-schema (3.0.0)
addressable (>= 2.8) addressable (>= 2.8)
@ -211,7 +213,7 @@ GEM
matrix (0.4.2) matrix (0.4.2)
method_source (1.0.0) method_source (1.0.0)
mini_mime (1.1.2) mini_mime (1.1.2)
minitest (5.15.0) minitest (5.16.1)
msgpack (1.5.2) msgpack (1.5.2)
net-imap (0.2.3) net-imap (0.2.3)
digest digest
@ -252,7 +254,7 @@ GEM
parallel (1.22.1) parallel (1.22.1)
parser (3.1.2.0) parser (3.1.2.0)
ast (~> 2.4.1) ast (~> 2.4.1)
pg (1.3.5) pg (1.4.1)
possessive (1.0.1) possessive (1.0.1)
postcodes_io (0.4.0) postcodes_io (0.4.0)
excon (~> 0.39) excon (~> 0.39)
@ -276,8 +278,8 @@ GEM
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-mini-profiler (2.3.4) rack-mini-profiler (2.3.4)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-test (1.1.0) rack-test (2.0.1)
rack (>= 1.0, < 3) rack (>= 1.3)
rails (7.0.3) rails (7.0.3)
actioncable (= 7.0.3) actioncable (= 7.0.3)
actionmailbox (= 7.0.3) actionmailbox (= 7.0.3)
@ -311,7 +313,7 @@ GEM
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
redcarpet (3.5.1) redcarpet (3.5.1)
redis (4.6.0) redis (4.7.0)
regexp_parser (2.5.0) regexp_parser (2.5.0)
request_store (1.5.1) request_store (1.5.1)
rack (>= 1.4) rack (>= 1.4)
@ -371,7 +373,7 @@ GEM
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
selenium-webdriver (4.2.1) selenium-webdriver (4.3.0)
childprocess (>= 0.5, < 5.0) childprocess (>= 0.5, < 5.0)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
@ -399,9 +401,9 @@ GEM
timeout (0.3.0) timeout (0.3.0)
tzinfo (2.0.4) tzinfo (2.0.4)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
uk_postcode (2.1.7) uk_postcode (2.1.8)
unicode-display_width (2.1.0) unicode-display_width (2.2.0)
view_component (2.49.1) view_component (2.56.2)
activesupport (>= 5.0.0, < 8.0) activesupport (>= 5.0.0, < 8.0)
method_source (~> 1.0) method_source (~> 1.0)
warden (1.2.9) warden (1.2.9)
@ -450,7 +452,6 @@ DEPENDENCIES
listen (~> 3.3) listen (~> 3.3)
notifications-ruby-client notifications-ruby-client
overcommit (>= 0.37.0) overcommit (>= 0.37.0)
pagy
paper_trail paper_trail
paper_trail-globalid paper_trail-globalid
pg (~> 1.1) pg (~> 1.1)

270
README.md

@ -1,259 +1,37 @@
# Submit social housing lettings and sales data (CORE) # Submit social housing lettings and sales data (CORE)
[![Production CI/CD Pipeline](https://github.com/communitiesuk/mhclg-data-collection-beta/actions/workflows/production_pipeline.yml/badge.svg)](https://github.com/communitiesuk/mhclg-data-collection-beta/actions/workflows/production_pipeline.yml) [![Production CI/CD Pipeline](https://github.com/communitiesuk/submit-social-housing-lettings-and-sales-data/actions/workflows/production_pipeline.yml/badge.svg)](https://github.com/communitiesuk/submit-social-housing-lettings-and-sales-data/actions/workflows/production_pipeline.yml)
[![Staging CI/CD Pipeline](https://github.com/communitiesuk/mhclg-data-collection-beta/actions/workflows/staging_pipeline.yml/badge.svg)](https://github.com/communitiesuk/mhclg-data-collection-beta/actions/workflows/staging_pipeline.yml) [![Staging CI/CD Pipeline](https://github.com/communitiesuk/submit-social-housing-lettings-and-sales-data/actions/workflows/staging_pipeline.yml/badge.svg)](https://github.com/communitiesuk/submit-social-housing-lettings-and-sales-data/actions/workflows/staging_pipeline.yml)
Codebase for the Ruby on Rails app that handles the submission of lettings and sales of social housing data in England. Ruby on Rails app that handles the submission of lettings and sales of social housing data in England. Currently in private beta.
## API documentation ## Domain documentation
API documentation can be found here: <https://communitiesuk.github.io/mhclg-data-collection-beta>. This is driven by [OpenAPI docs](docs/api/DLUHC-CORE-Data.v1.json)
## Required Setup
Pre-requisites:
- Ruby
- Rails
- Postgres
### Quick start
1. Copy the `.env.example` to `.env` and replace the database credentials with your local postgres user credentials.
2. Install the dependencies:\
`bundle install`
3. Create the database:\
`rake db:create`
4. Run the database migrations:\
`rake db:migrate`
5. Seed the database if required:\
`rake db:seed`
6. Seed the database with rent ranges if required (~7000 rows per year):\
`rake "data_import:rent_ranges[<start_year>,<rent_ranges_path>]"`
For 2021-2022 ranges run:\
`rake "data_import:rent_ranges[2021,config/rent_range_data/2021.csv]"`
7. Install the frontend depenencies:\
`yarn install`
8. Start the dev servers using foreman:\
`./bin/dev`
Or start them individually:\
a. Rails:\
`bundle exec rails s`
b. JS (for hot reloading):\
`yarn build --mode=development --watch`
If you're not modifying front end assets you can bundle them as a one off task:\
`yarn build --mode=development`
Development mode will target the latest versions of Chrome, Firefox and Safari for transpilation while production mode will target older browsers.
The Rails server will start on <http://localhost:3000>.
Running the test suite (front end assets need to be built or server needs to be running):\
`bundle exec rspec`
### Using Docker
1. Build the image:\
`docker-compose build`
2. Run the database migrations:\
`docker-compose run --rm app /bin/bash -c 'rake db:migrate'`
3. Seed the database if required:\
`docker-compose run --rm app /bin/bash -c 'rake db:seed'`
4. To be able to debug with Pry run the app using:\
`docker-compose run --service-ports app`
If this is not needed you can run `docker-compose up` as normal
The Rails server will start on <http://localhost:8080>.
## Infrastructure
This application is running on [GOV.UK PaaS](https://www.cloud.service.gov.uk/). To deploy you need to:
1. Contact your organisation manager to get an account in `dluhc-core` organization and in the relevant spaces (staging/production).
2. [Install the Cloud Foundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html)
3. Login:\
`cf login -a api.london.cloud.service.gov.uk -u <your_username>`
4. Set your deployment target (staging/production):\ - [Service overview](docs/service_overview.md)
`cf target -o dluhc-core -s <deploy_environment>` - [Organisations](docs/organisations.md)
- [Users and roles](docs/users.md)
- [Supported housing schemes](docs/schemes.md)
5. Deploy:\ ## Technical Documentation
`cf push dluhc-core --strategy rolling`. This will use the [manifest file](staging_manifest.yml)
Once the app is deployed: - [Developer setup](docs/developer_setup.md)
- [Frontend](docs/frontend.md)
- [Testing strategy](docs/testing.md)
- [Form Builder](docs/form_builder.md)
- [Form Runner](docs/form_runner.md)
- [Infrastructure](docs/infrastructure.md)
- [Monitoring](docs/monitoring.md)
- [Exporting to CDS](docs/exports)
- [Architecture decision records](docs/adr)
1. Get a Rails console:\ ## API documentation
`cf ssh dluhc-core-staging -t -c "/tmp/lifecycle/launcher /home/vcap/app 'rails console' ''"`
2. Check logs:\
`cf logs dluhc-core-staging --recent`
### Troubleshooting deployments
A failed Github deployment action will occasionally leave a Cloud Foundry deployment in a broken state. As a result all subsequent Github deployment actions will also fail with the message `Cannot update this process while a deployment is in flight`.
`
cf cancel-deployment dluhc-core
`
You'd then need to check the logs and fix the issue that caused the initial deployment to fail.
## CI/CD
When a commit is made to `main` the following GitHub action jobs are triggered:
1. **Test**: RSpec runs our test suite
2. **Deploy**: If the Test stage passes, this job will deploy the app to our GOV.UK PaaS account using the Cloud Foundry CLI
When a pull request is opened to `main` only the Test stage runs.
## Frontend
### GOV.UK Design System components
This service follows the guidance and recommendations from the [GOV.UK Design System](https://design-system.service.gov.uk). This is achieved using the following libraries:
- **GOV.UK Frontend** – CSS and JavaScript for all Design System components\
[Documentation](https://frontend.design-system.service.gov.uk) ·
[GitHub](https://github.com/alphagov/govuk-frontend)
- **GOV.UK Components** – Rails view components for non-form related Design System components\
[Documentation](https://govuk-components.netlify.app) ·
[Github](https://github.com/DFE-Digital/govuk-components) ·
[RubyDoc](https://www.rubydoc.info/gems/govuk-components)
- **GOV.UK FormBuilder** Rails form builder for form related Design System components\
[Documentation](https://govuk-form-builder.netlify.app) ·
[GitHub](https://github.com/DFE-Digital/govuk-formbuilder) ·
[RubyDoc](https://www.rubydoc.info/gems/govuk_design_system_formbuilder)
### Service-specific components
Service-specific components are built using the [ViewComponent](https://viewcomponent.org) framework, and can be found in `app/components`.
Components use HTML class names that follow the BEM methodology. We use the `app-*` prefix to prevent collisions with components provided by the Design System (which uses `govuk-*`). See [Extending and modifying components in production](https://design-system.service.gov.uk/get-started/extending-and-modifying-components/).
Stylesheets are written using [Sass](https://sass-lang.com) (and the SCSS syntax), using the mixins and helpers provided by [govuk-frontend](https://frontend.design-system.service.gov.uk/sass-api-reference/).
Separate stylesheets are used for each component, with filenames that match the component’s namespace.
Like the components provided by the Design System, components are progressively enhanced. We use [Stimulus](https://stimulus.hotwired.dev) to add any client-side JavaScript enhancements.
## Single log submission form configuration
The form for this is driven by a JSON file in `/config/forms/{start_year}_{end_year}.json`
The JSON should follow the structure:
```jsonc
{
"form_type": "lettings" / "sales",
"start_year": Integer, // i.e. 2020
"end_year": Integer, // i.e. 2021
"sections": {
"[snake_case_section_name_string]": {
"label": String,
"description": String,
"subsections": {
"[snake_case_subsection_name_string]": {
"label": String,
"pages": {
"[snake_case_page_name_string]": {
"header": String,
"description": String,
"questions": {
"[snake_case_question_name_string]": {
"header": String,
"hint_text": String,
"check_answer_label": String,
"type": "text" / "numeric" / "radio" / "checkbox" / "date",
"min": Integer, // numeric only
"max": Integer, // numeric only
"step": Integer, // numeric only
"width": 2 / 3 / 4 / 5 / 10 / 20, // text and numeric only
"prefix": String, // numeric only
"suffix": String, //numeric only
"answer_options": { // checkbox and radio only
"0": String,
"1": String
},
"conditional_for": {
"[snake_case_question_to_enable_1_name_string]": ["condition-that-enables"],
"[snake_case_question_to_enable_2_name_string]": ["condition-that-enables"]
},
"inferred_answers": { "field_that_gets_inferred_from_current_field": { "is_that_field_inferred": true } },
"inferred_check_answers_value": {
"condition": { "field_name_for_inferred_check_answers_condition": "field_value_for_inferred_check_answers_condition" },
"value": "Inferred value that gets displayed if condition is met"
}
}
},
"depends_on": [{ "question_key": "answer_value_required_for_this_page_to_be_shown" }]
}
}
}
}
}
}
}
```
Assumptions made by the format:
- All forms have at least 1 section
- All sections have at least 1 subsection
- All subsections have at least 1 page
- All pages have at least 1 question
- The ActiveRecord case log model has a field for each question name (must match). In the case of checkbox questions it must have one field for every answer option (again names must match).
- Text not required by a page/question such as a header or hint text should be passed as an empty string
- For conditionally shown questions, conditions that have been implemented and can be used are:
- Radio question answer option selected matches one of conditional e.g. ["answer-options-1-string", "answer-option-3-string"]
- Numeric question value matches condition e.g. [">2"], ["<7"] or ["== 6"]
- When the top level question is a radio button and the conditional question is a numeric, text or date field then the conditional question is shown inline
- When the conditional question is a radio, checkbox or select field it should be displayed on it's own page and "depends_on" should be used rather than "conditional_for"
Page routing:
- Form navigation works by stepping sequentially through every page defined in the JSON form definition for the given subsection. For every page it checks if it has "depends_on" conditions. If it does, it evaluates them to determine whether that page should be show or not.
- In this way we can build up whole branches by having:
```jsonc
"page_1": { "questions": { "question_1: "answer_options": ["A", "B"] } },
"page_2": { "questions": { "question_2: "answer_options": ["C", "D"] }, "depends_on": [{ "question_1": "A" }] },
"page_3": { "questions": { "question_3: "answer_options": ["E", "F"] }, "depends_on": [{ "question_1": "A" }] },
"page_4": { "questions": { "question_4: "answer_options": ["G", "H"] }, "depends_on": [{ "question_1": "B" }] },
```
### JSON form validation against Schema
To validate the form JSON against the schema you can run:\ API documentation can be found here: <https://communitiesuk.github.io/submit-social-housing-lettings-and-sales-data>. This is driven by [OpenAPI docs](docs/api/DLUHC-CORE-Data.v1.json)
`rake form_definition:validate["config/forms/2021_2022.json"]`
n.b. You may have to escape square brackets in zsh\ ## System architecture
`rake form_definition:validate\["config/forms/2021_2022.json"\]`
This will validate the given form definition against the schema in `config/forms/schema/generic.json`. ![View of system architecture](docs/images/architecture.png)
You can also run:\ ## User interface
`rake form_definition:validate_all`
This will validate all forms in directories = `["config/forms", "spec/fixtures/forms"]` ![View of the logs list](docs/images/service.png)

105
app/controllers/schemes_controller.rb

@ -7,6 +7,7 @@ class SchemesController < ApplicationController
before_action :authenticate_scope! before_action :authenticate_scope!
def index def index
flash[:notice] = "#{Scheme.find(params[:scheme_id].to_i).service_name} has been created." if params[:scheme_id]
redirect_to schemes_organisation_path(current_user.organisation) unless current_user.support? redirect_to schemes_organisation_path(current_user.organisation) unless current_user.support?
all_schemes = Scheme.all all_schemes = Scheme.all
@ -25,20 +26,120 @@ class SchemesController < ApplicationController
@total_count = @scheme.locations.size @total_count = @scheme.locations.size
end end
def new
@scheme = Scheme.new
end
def create
@scheme = Scheme.new(scheme_params)
if @scheme.save
render "schemes/primary_client_group"
else
@scheme.errors.add(:organisation_id, message: @scheme.errors[:organisation])
@scheme.errors.delete(:organisation)
render :new, status: :unprocessable_entity
end
end
def update
check_answers = params[:scheme][:check_answers]
page = params[:scheme][:page]
if @scheme.update(scheme_params)
if check_answers
if confirm_secondary_page? page
redirect_to scheme_secondary_client_group_path(@scheme, check_answers: "true")
else
@scheme.update!(secondary_client_group: nil) if @scheme.has_other_client_group == "No"
redirect_to scheme_check_answers_path(@scheme)
end
else
redirect_to next_page_path params[:scheme][:page]
end
else
render request.current_url, status: :unprocessable_entity
end
end
def primary_client_group
render "schemes/primary_client_group"
end
def confirm_secondary_client_group
render "schemes/confirm_secondary"
end
def secondary_client_group
render "schemes/secondary_client_group"
end
def support
render "schemes/support"
end
def details
render "schemes/details"
end
def check_answers
render "schemes/check_answers"
end
private private
def confirm_secondary_page?(page)
page == "confirm-secondary" && @scheme.has_other_client_group == "Yes"
end
def next_page_path(page)
case page
when "primary-client-group"
scheme_confirm_secondary_client_group_path(@scheme)
when "confirm-secondary"
@scheme.has_other_client_group == "Yes" ? scheme_secondary_client_group_path(@scheme) : scheme_support_path(@scheme)
when "secondary-client-group"
scheme_support_path(@scheme)
when "support"
scheme_check_answers_path(@scheme)
when "details"
scheme_primary_client_group_path(@scheme)
end
end
def scheme_params
required_params = params.require(:scheme).permit(:service_name,
:sensitive,
:organisation_id,
:stock_owning_organisation_id,
:scheme_type,
:registered_under_care_act,
:total_units,
:id,
:has_other_client_group,
:primary_client_group,
:secondary_client_group,
:support_type,
:intended_stay)
required_params[:sensitive] = required_params[:sensitive].to_i if required_params[:sensitive]
if current_user.data_coordinator?
required_params[:organisation_id] = current_user.organisation_id
end
required_params
end
def search_term def search_term
params["search"] params["search"]
end end
def find_resource def find_resource
@scheme = Scheme.find_by(id: params[:id]) @scheme = Scheme.find_by(id: params[:id]) || Scheme.find_by(id: params[:scheme_id])
end end
def authenticate_scope! def authenticate_scope!
head :unauthorized and return unless current_user.data_coordinator? || current_user.support? head :unauthorized and return unless current_user.data_coordinator? || current_user.support?
if %w[show locations].include?(action_name) && !((current_user.organisation == @scheme.organisation) || current_user.support?) if %w[show locations primary_client_group confirm_secondary_client_group secondary_client_group support details check_answers].include?(action_name) && !((current_user.organisation == @scheme.organisation) || current_user.support?)
render_not_found and return render_not_found and return
end end
end end

8
app/frontend/application.js

@ -12,12 +12,12 @@ import 'custom-event-polyfill'
import 'intersection-observer' import 'intersection-observer'
// //
import GOVUKFrontend from 'govuk-frontend' import { initAll as GOVUKFrontend } from 'govuk-frontend'
import GOVUKPrototypeComponents from 'govuk-prototype-components' import { initAll as GOVUKPrototypeComponents } from 'govuk-prototype-components'
import './styles/application.scss' import './styles/application.scss'
import './controllers' import './controllers'
require.context('govuk-frontend/govuk/assets') require.context('govuk-frontend/govuk/assets')
GOVUKFrontend.initAll() GOVUKFrontend()
GOVUKPrototypeComponents.initAll() GOVUKPrototypeComponents()

8
app/frontend/controllers/govukfrontend_controller.js

@ -1,10 +1,10 @@
import GOVUKFrontend from 'govuk-frontend' import { initAll as GOVUKFrontend } from 'govuk-frontend'
import GOVUKPrototypeComponents from 'govuk-prototype-components' import { initAll as GOVUKPrototypeComponents } from 'govuk-prototype-components'
import { Controller } from '@hotwired/stimulus' import { Controller } from '@hotwired/stimulus'
export default class extends Controller { export default class extends Controller {
connect () { connect () {
GOVUKFrontend.initAll() GOVUKFrontend()
GOVUKPrototypeComponents.initAll() GOVUKPrototypeComponents()
} }
} }

134
app/frontend/styles/_pagination.scss

@ -1,134 +0,0 @@
// https://github.com/alphagov/govuk-frontend/blob/add-pagination-prototype/src/govuk/components/pagination/_index.scss
.app-pagination {
border-top: 1px solid $govuk-border-colour;
margin-top: govuk-spacing(2);
padding-top: govuk-spacing(2);
text-align: center;
@include govuk-media-query($from: tablet) {
// Hide whitespace between elements
font-size: 0;
// Trick to remove the need for floats
text-align: justify;
&:after {
content: " ";
display: inline-block;
width: 100%;
}
}
}
.app-pagination__list {
margin: 0 govuk-spacing(-3);
padding: 0;
list-style: none;
@include govuk-media-query($from: tablet) {
display: inline-block;
margin-bottom: 0;
}
}
.app-pagination__results {
@include govuk-font(19);
margin-top: 0;
margin-bottom: govuk-spacing(4);
padding: govuk-spacing(1) 0;
@include govuk-media-query($from: tablet) {
display: inline-block;
}
}
.app-pagination__item {
@include govuk-font(19);
display: inline-block;
margin-bottom: govuk-spacing(4);
// Hide items on small screens
@include govuk-media-query($until: tablet) {
display: none;
}
}
// Only show previous, next, first, last and current items on mobile
.app-pagination__item--current,
.app-pagination__item--divider,
.app-pagination__item--prev,
.app-pagination__item--next,
.app-pagination__item:nth-child(2),
.app-pagination__item:nth-last-child(2) {
@include govuk-media-query($until: tablet) {
display: inline-block;
}
}
.app-pagination__item--current,
.app-pagination__item--divider {
box-sizing: border-box;
font-weight: bold;
min-width: govuk-spacing(8);
min-height: govuk-spacing(4);
padding: govuk-spacing(2);
text-align: center;
}
.app-pagination__item--divider {
margin: 0 govuk-spacing(-4);
padding-right: 0;
padding-left: 0;
color: $govuk-secondary-text-colour;
pointer-events: none;
}
.app-pagination__link {
@include govuk-link-common;
@include govuk-link-style-no-underline;
box-sizing: border-box;
color: $govuk-link-colour;
display: block;
min-width: govuk-spacing(8);
min-height: govuk-spacing(4);
padding: govuk-spacing(2);
text-align: center;
.app-pagination__link-label {
@include govuk-font($size: 16, $weight: "regular");
display: block;
padding-left: 32px;
text-decoration: underline;
}
&:hover .app-pagination__link-label {
@include govuk-link-hover-decoration;
}
&:focus {
box-shadow: 0 0 $govuk-focus-colour, 0 4px $govuk-focus-text-colour;
text-decoration: underline;
}
&:hover:not(:focus) {
background-color: govuk-colour("light-grey", $legacy: "grey-4");
}
}
.app-pagination__icon {
fill: currentcolor;
}
.app-pagination__item--prev .app-pagination__link,
.app-pagination__item--next .app-pagination__link {
padding: govuk-spacing(2) govuk-spacing(3);
font-weight: bold;
}
.app-pagination__item--prev .app-pagination__icon {
margin-right: govuk-spacing(2);
}
.app-pagination__item--next .app-pagination__icon {
margin-left: govuk-spacing(2);
}

1
app/frontend/styles/application.scss

@ -36,7 +36,6 @@ $govuk-breakpoints: (
@import "tag"; @import "tag";
@import "task-list"; @import "task-list";
@import "template"; @import "template";
@import "pagination";
@import "panel"; @import "panel";
@import "primary-navigation"; @import "primary-navigation";
@import "search"; @import "search";

2
app/helpers/details_table_helper.rb

@ -4,7 +4,7 @@ module DetailsTableHelper
list = attribute[:value].map { |value| "<li>#{value}</li>" }.join list = attribute[:value].map { |value| "<li>#{value}</li>" }.join
simple_format(list, { class: "govuk-list govuk-list--bullet" }, wrapper_tag: "ul") simple_format(list, { class: "govuk-list govuk-list--bullet" }, wrapper_tag: "ul")
else else
value = attribute[:value].is_a?(Array) ? attribute[:value].first : attribute[:value] value = attribute[:value].is_a?(Array) ? attribute[:value].first : attribute[:value] || "None"
simple_format(value.to_s, { class: "govuk-body" }, wrapper_tag: "p") simple_format(value.to_s, { class: "govuk-body" }, wrapper_tag: "p")
end end

4
app/helpers/tab_nav_helper.rb

@ -7,8 +7,8 @@ module TabNavHelper
end end
def scheme_cell(scheme) def scheme_cell(scheme)
link_text = scheme.service_name.presence link_text = scheme.service_name
[govuk_link_to(link_text, scheme), "<span class=\"govuk-visually-hidden\">Scheme </span><span class=\"govuk-!-font-weight-regular app-!-colour-muted\">#{scheme.primary_client_group_display}</span>"].join("\n") [govuk_link_to(link_text, scheme), "<span class=\"govuk-visually-hidden\">Scheme </span><span class=\"govuk-!-font-weight-regular app-!-colour-muted\">#{scheme.primary_client_group}</span>"].join("\n")
end end
def org_cell(user) def org_cell(user)

3
app/models/organisation.rb

@ -4,7 +4,8 @@ class Organisation < ApplicationRecord
has_many :managed_case_logs, class_name: "CaseLog", foreign_key: "managing_organisation_id" has_many :managed_case_logs, class_name: "CaseLog", foreign_key: "managing_organisation_id"
has_many :data_protection_confirmations has_many :data_protection_confirmations
has_many :organisation_rent_periods has_many :organisation_rent_periods
has_many :schemes has_many :owned_schemes, class_name: "Scheme", foreign_key: "stock_owning_organisation_id"
has_many :managed_schemes, class_name: "Scheme"
scope :search_by_name, ->(name) { where("name ILIKE ?", "%#{name}%") } scope :search_by_name, ->(name) { where("name ILIKE ?", "%#{name}%") }
scope :search_by, ->(param) { search_by_name(param) } scope :search_by, ->(param) { search_by_name(param) }

182
app/models/scheme.rb

@ -1,109 +1,145 @@
class Scheme < ApplicationRecord class Scheme < ApplicationRecord
belongs_to :organisation belongs_to :organisation
belongs_to :stock_owning_organisation, optional: true, class_name: "Organisation"
has_many :locations has_many :locations
has_many :case_logs has_many :case_logs
scope :search_by_code, ->(code) { where("code ILIKE ?", "%#{code}%") } scope :filter_by_id, ->(id) { where(id: (id.start_with?("S") ? id[1..] : id)) }
scope :search_by_service_name, ->(name) { where("service_name ILIKE ?", "%#{name}%") } scope :search_by_service_name, ->(name) { where("service_name ILIKE ?", "%#{name}%") }
scope :search_by, ->(param) { search_by_code(param).or(search_by_service_name(param)) } scope :search_by_postcode, ->(postcode) { joins(:locations).where("locations.postcode ILIKE ?", "%#{postcode.delete(' ')}%") }
scope :search_by, ->(param) { search_by_postcode(param).or(search_by_service_name(param)).or(filter_by_id(param)).distinct }
SCHEME_TYPE = { SENSITIVE = {
0 => "Missings", No: 0,
4 => "Foyer", Yes: 1,
5 => "Direct Access Hostel",
6 => "Other Supported Housing",
7 => "Housing for older people",
}.freeze }.freeze
PRIMARY_CLIENT_GROUP = { enum sensitive: SENSITIVE, _suffix: true
"O" => "Homeless families with support needs",
"H" => "Offenders & people at risk of offending", REGISTERED_UNDER_CARE_ACT = {
"M" => "Older people with support needs", "No": 0,
"L" => "People at risk of domestic violence", "Yes – registered care home providing nursing care": 1,
"A" => "People with a physical or sensory disability", "Yes – registered care home providing personal care": 2,
"G" => "People with alcohol problems", "Yes – part registered as a care home": 3,
"F" => "People with drug problems",
"B" => "People with HIV or AIDS",
"D" => "People with learning disabilities",
"E" => "People with mental health problems",
"I" => "Refugees (permanent)",
"S" => "Rough sleepers",
"N" => "Single homeless people with support needs",
"R" => "Teenage parents",
"Q" => "Young people at risk",
"P" => "Young people leaving care",
"X" => "Missing",
}.freeze }.freeze
enum registered_under_care_act: REGISTERED_UNDER_CARE_ACT
SCHEME_TYPE = {
"Missing": 0,
"Foyer": 4,
"Direct Access Hostel": 5,
"Other Supported Housing": 6,
"Housing for older people": 7,
}.freeze
enum scheme_type: SCHEME_TYPE, _suffix: true
SUPPORT_TYPE = { SUPPORT_TYPE = {
0 => "Missing", "Missing": 0,
1 => "Resettlement Support", "Resettlement support": 1,
2 => "Low levels of support", "Low levels of support": 2,
3 => "Medium levels of support", "Medium levels of support": 3,
4 => "High levels of care and support", "High levels of care and support": 4,
5 => "Nursing care services to a care home", "Nursing care services to a care home": 5,
6 => "Floating Support", "Floating Support": 6,
}.freeze }.freeze
INTENDED_STAY = { enum support_type: SUPPORT_TYPE, _suffix: true
"M" => "Medium stay",
"P" => "Permanent", PRIMARY_CLIENT_GROUP = {
"S" => "Short Stay", "Homeless families with support needs": "O",
"V" => "Very short stay", "Offenders & people at risk of offending": "H",
"X" => "Missing", "Older people with support needs": "M",
"People at risk of domestic violence": "L",
"People with a physical or sensory disability": "A",
"People with alcohol problems": "G",
"People with drug problems": "F",
"People with HIV or AIDS": "B",
"People with learning disabilities": "D",
"People with mental health problems": "E",
"Refugees (permanent)": "I",
"Rough sleepers": "S",
"Single homeless people with support needs": "N",
"Teenage parents": "R",
"Young people at risk": "Q",
"Young people leaving care": "P",
"Missing": "X",
}.freeze }.freeze
REGISTERED_UNDER_CARE_ACT = { enum primary_client_group: PRIMARY_CLIENT_GROUP, _suffix: true
0 => "No", enum secondary_client_group: PRIMARY_CLIENT_GROUP, _suffix: true
1 => "Yes – part registered as a care home",
INTENDED_STAY = {
"Medium stay": "M",
"Permanent": "P",
"Short stay": "S",
"Very short stay": "V",
"Missing": "X",
}.freeze }.freeze
SENSITIVE = { HAS_OTHER_CLIENT_GROUP = {
0 => "No", No: 0,
1 => "Yes", Yes: 1,
}.freeze }.freeze
def display_attributes enum intended_stay: INTENDED_STAY, _suffix: true
enum has_other_client_group: HAS_OTHER_CLIENT_GROUP, _suffix: true
def id_to_display
"S#{id}"
end
def check_details_attributes
[ [
{ name: "Service code", value: code }, { name: "Service code", value: id_to_display },
{ name: "Name", value: service_name }, { name: "Name", value: service_name },
{ name: "Confidential information", value: sensitive_display }, { name: "Confidential information", value: sensitive },
{ name: "Housing stock owned by", value: stock_owning_organisation&.name },
{ name: "Managed by", value: organisation.name }, { name: "Managed by", value: organisation.name },
{ name: "Type of scheme", value: scheme_type_display }, { name: "Type of scheme", value: scheme_type },
{ name: "Registered under Care Standards Act 2000", value: registered_under_care_act_display }, { name: "Registered under Care Standards Act 2000", value: registered_under_care_act },
{ name: "Total number of units", value: total_units },
{ name: "Primary client group", value: primary_client_group_display },
{ name: "Secondary client group", value: secondary_client_group_display },
{ name: "Level of support given", value: support_type_display },
{ name: "Intended length of stay", value: intended_stay_display },
] ]
end end
def scheme_type_display def check_primary_client_attributes
SCHEME_TYPE[scheme_type] [
end { name: "Primary client group", value: primary_client_group },
]
def sensitive_display
SENSITIVE[sensitive]
end
def registered_under_care_act_display
REGISTERED_UNDER_CARE_ACT[registered_under_care_act]
end end
def primary_client_group_display def check_secondary_client_confirmation_attributes
PRIMARY_CLIENT_GROUP[primary_client_group] [
{ name: "Has another client group", value: has_other_client_group },
]
end end
def secondary_client_group_display def check_secondary_client_attributes
PRIMARY_CLIENT_GROUP[secondary_client_group] [
{ name: "Secondary client group", value: secondary_client_group },
]
end end
def support_type_display def check_support_attributes
SUPPORT_TYPE[support_type] [
{ name: "Level of support given", value: support_type },
{ name: "Intended length of stay", value: intended_stay },
]
end end
def intended_stay_display def display_attributes
INTENDED_STAY[intended_stay] [
{ name: "Service code", value: id_to_display },
{ name: "Name", value: service_name },
{ name: "Confidential information", value: sensitive },
{ name: "Housing stock owned by", value: stock_owning_organisation&.name },
{ name: "Managed by", value: organisation.name },
{ name: "Type of scheme", value: scheme_type },
{ name: "Registered under Care Standards Act 2000", value: registered_under_care_act },
{ name: "Primary client group", value: primary_client_group },
{ name: "Secondary client group", value: secondary_client_group },
{ name: "Level of support given", value: support_type },
{ name: "Intended length of stay", value: intended_stay },
]
end end
end end

8
app/models/validations/financial_validations.rb

@ -107,11 +107,13 @@ module Validations::FinancialValidations
period = record.form.get_question("period", record).label_from_value(record.period).downcase period = record.form.get_question("period", record).label_from_value(record.period).downcase
if record.chcharge.blank? if record.chcharge.blank?
record.errors.add :chcharge, I18n.t("validations.financial.carehome.not_provided", period:) record.errors.add :chcharge, I18n.t("validations.financial.carehome.not_provided", period:)
elsif !weekly_value_in_range(record, "chcharge", 0, 1000) elsif !weekly_value_in_range(record, "chcharge", 10, 1000)
max_chcharge = record.weekly_to_value_per_period(1000) max_chcharge = record.weekly_to_value_per_period(1000)
min_chcharge = record.weekly_to_value_per_period(10)
max_chcharge = [record.form.get_question("chcharge", record).prefix, max_chcharge].join("") if record.form.get_question("chcharge", record).present? max_chcharge = [record.form.get_question("chcharge", record).prefix, max_chcharge].join("") if record.form.get_question("chcharge", record).present?
record.errors.add :period, I18n.t("validations.financial.carehome.over_1000", period:, min_chcharge: "£0", max_chcharge:) min_chcharge = [record.form.get_question("chcharge", record).prefix, min_chcharge].join("") if record.form.get_question("chcharge", record).present?
record.errors.add :chcharge, I18n.t("validations.financial.carehome.over_1000", period:, min_chcharge: "£0", max_chcharge:) record.errors.add :period, I18n.t("validations.financial.carehome.out_of_range", period:, min_chcharge:, max_chcharge:)
record.errors.add :chcharge, I18n.t("validations.financial.carehome.out_of_range", period:, min_chcharge:, max_chcharge:)
end end
end end
end end

2
app/views/layouts/_footer.html.erb

@ -22,7 +22,7 @@
<h3 class="govuk-heading-s govuk-!-margin-bottom-1">Email</h3> <h3 class="govuk-heading-s govuk-!-margin-bottom-1">Email</h3>
<ul class="govuk-list govuk-!-font-size-16 govuk-!-margin-bottom-0"> <ul class="govuk-list govuk-!-font-size-16 govuk-!-margin-bottom-0">
<li> <li>
<%= govuk_mail_to("submitcoredata@levellingup.gov.uk", class: "govuk-footer__link") %> <%= govuk_mail_to("dluhc.digital-services@levellingup.gov.uk", subject: "CORE:", class: "govuk-footer__link") %>
</li> </li>
<li class="govuk-!-margin-bottom-0">We aim to respond within 2 working&nbsp;days</li> <li class="govuk-!-margin-bottom-0">We aim to respond within 2 working&nbsp;days</li>
</ul> </ul>

4
app/views/organisations/schemes.html.erb

@ -11,9 +11,11 @@
) %> ) %>
<% end %> <% end %>
<%= govuk_button_link_to "Create a new supported housing scheme", new_scheme_path, html: { method: :post } %>
<h2 class="govuk-visually-hidden">Supported housing schemes</h2> <h2 class="govuk-visually-hidden">Supported housing schemes</h2>
<%= render SearchComponent.new(current_user:, search_label: "Search by scheme name or code", value: @searched) %> <%= render SearchComponent.new(current_user:, search_label: "Search by scheme name, code or postcode", value: @searched) %>
<hr class="govuk-section-break govuk-section-break--visible govuk-section-break--m"> <hr class="govuk-section-break govuk-section-break--visible govuk-section-break--m">

39
app/views/pagy/_nav.html.erb

@ -1,41 +1,6 @@
<% link = pagy_link_proc(pagy) -%> <%= govuk_pagination(pagy:) %>
<% if pagy.pages > 1 %> <% if pagy.pages > 1 %>
<nav class="app-pagination" id="pagination-label" aria-label="results navigation"> <p class="govuk-body">
<ul class="app-pagination__list">
<li class="app-pagination__item app-pagination__item--prev">
<% if pagy.prev %>
<a class="app-pagination__link" href="<%= "#{request.path}?page=#{pagy.prev}" %>">
<% end %>
<span class="app-pagination__link-title">
<svg class="app-pagination__icon" xmlns="http://www.w3.org/2000/svg" height="13" width="17">
<path d="m6.5938-0.0078125-6.7266 6.7266 6.7441 6.4062 1.377-1.449-4.1856-3.9768h12.896v-2h-12.984l4.2931-4.293-1.414-1.414z"></path>
</svg>Previous
<span class="govuk-visually-hidden">page</span>
</span></a>
</li>
<% pagy.series.each do |item| %>
<% if item == :gap %>
<li class="app-pagination__item app-pagination__item--ellipses">…</li>
<% elsif item.is_a?(String) %>
<li class="app-pagination__item app-pagination__item--current"><span class="govuk-visually-hidden">Page </span><%= item %><span class="govuk-visually-hidden"> (current page) </span></li>
<% else %>
<li class="app-pagination__item"><a class="app-pagination__link" href="<%= "#{request.path}?page=#{item}" %>"><span class="govuk-visually-hidden">Page </span><%= item %></a></li>
<% end %>
<% end %>
<li class="app-pagination__item app-pagination__item--next">
<% if pagy.next %>
<a class="app-pagination__link" href="<%= "#{request.path}?page=#{pagy.next}" %>">
<% end %>
Next <span class="govuk-visually-hidden">page</span>
<span class="app-pagination__link-title">
<svg class="app-pagination__icon" xmlns="http://www.w3.org/2000/svg" height="13" width="17">
<path d="m10.107-0.0078125-1.4136 1.414 4.2926 4.293h-12.986v2h12.896l-4.1855 3.9766 1.377 1.4492 6.7441-6.4062-6.7246-6.7266z"></path>
</svg>
</span></a>
</li>
</ul>
<p class="app_pagination__results">
Showing <b><%= pagy.from %></b> to <b><%= pagy.to %></b> of <b><%= pagy.count %></b> <%= item_name %> Showing <b><%= pagy.from %></b> to <b><%= pagy.to %></b> of <b><%= pagy.count %></b> <%= item_name %>
</p> </p>
</nav>
<% end %> <% end %>

2
app/views/schemes/_scheme_list.html.erb

@ -22,7 +22,7 @@
<% @schemes.each do |scheme| %> <% @schemes.each do |scheme| %>
<%= table.body do |body| %> <%= table.body do |body| %>
<%= body.row do |row| %> <%= body.row do |row| %>
<% row.cell(text: scheme.code) %> <% row.cell(text: scheme.id_to_display) %>
<% row.cell(text: simple_format(scheme_cell(scheme), { class: "govuk-!-font-weight-bold" }, wrapper_tag: "div")) %> <% row.cell(text: simple_format(scheme_cell(scheme), { class: "govuk-!-font-weight-bold" }, wrapper_tag: "div")) %>
<% row.cell(text: scheme.organisation.name) %> <% row.cell(text: scheme.organisation.name) %>
<% row.cell(text: scheme.created_at.to_formatted_s(:govuk_date)) %> <% row.cell(text: scheme.created_at.to_formatted_s(:govuk_date)) %>

65
app/views/schemes/check_answers.html.erb

@ -0,0 +1,65 @@
<% content_for :title, "Check your answers before creating this scheme" %>
<%= render partial: "organisations/headings", locals: { main: "Check your changes before updating this scheme", sub: @scheme.service_name } %>
<%= govuk_tabs(title: "Check your answers before creating this scheme") do |component| %>
<% component.tab(label: "Scheme") do %>
<%= govuk_summary_list do |summary_list| %>
<% @scheme.check_details_attributes.each do |attr| %>
<% next if current_user.data_coordinator? && attr[:name] == ("Managed by") %>
<%= summary_list.row do |row| %>
<% row.key { attr[:name].to_s } %>
<% row.value { details_html(attr) } %>
<% row.action(
text: "Change",
href: scheme_details_path(scheme_id: @scheme.id, check_answers: true),
) %>
<% end %>
<% end %>
<% @scheme.check_primary_client_attributes.each do |attr| %>
<%= summary_list.row do |row| %>
<% row.key { attr[:name].to_s } %>
<% row.value { details_html(attr) } %>
<% row.action(
text: "Change",
href: scheme_primary_client_group_path(scheme_id: @scheme.id, check_answers: true),
) %>
<% end %>
<% end %>
<% @scheme.check_secondary_client_confirmation_attributes.each do |attr| %>
<%= summary_list.row do |row| %>
<% row.key { attr[:name].to_s } %>
<% row.value { details_html(attr) } %>
<% row.action(
text: "Change",
href: scheme_confirm_secondary_client_group_path(scheme_id: @scheme.id, check_answers: true),
) %>
<% end %>
<% end %>
<% if @scheme.has_other_client_group == "Yes" %>
<% @scheme.check_secondary_client_attributes.each do |attr| %>
<%= summary_list.row do |row| %>
<% row.key { attr[:name].to_s } %>
<% row.value { details_html(attr) } %>
<% row.action(
text: "Change",
href: scheme_secondary_client_group_path(scheme_id: @scheme.id, check_answers: true),
) %>
<% end %>
<% end %>
<% end %>
<% @scheme.check_support_attributes.each do |attr| %>
<%= summary_list.row do |row| %>
<% row.key { attr[:name].to_s } %>
<% row.value { details_html(attr) } %>
<% row.action(
text: "Change",
href: scheme_support_path(scheme_id: @scheme.id, check_answers: true),
) %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<%= govuk_button_link_to "Create scheme", schemes_path(scheme_id: @scheme.id), html: { method: :get } %>

32
app/views/schemes/confirm_secondary.html.erb

@ -0,0 +1,32 @@
<% content_for :title, "Does this scheme provide for another client group?" %>
<% content_for :before_content do %>
<%= govuk_back_link(
text: "Back",
href: request.query_parameters["check_answers"] ? "/schemes/#{@scheme.id}/check-answers" : "/schemes/#{@scheme.id}/primary-client-group",
) %>
<% end %>
<%= render partial: "organisations/headings", locals: { main: "Does this scheme provide for another client group?", sub: @scheme.service_name } %>
<%= form_for(@scheme, method: :patch) do |f| %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<% selection = [OpenStruct.new(id: "Yes", name: "Yes"), OpenStruct.new(id: "No", name: "No")] %>
<%= f.govuk_collection_radio_buttons :has_other_client_group,
selection,
:id,
:name,
legend: nil %>
<%= f.hidden_field :page, value: "confirm-secondary" %>
<% if request.query_parameters["check_answers"] == "true" %>
<%= f.hidden_field :check_answers, value: "true" %>
<% end %>
<%= f.govuk_submit "Save and continue" %>
</div>
</div>
<% end %>

76
app/views/schemes/details.html.erb

@ -0,0 +1,76 @@
<% content_for :title, "Create a new supported housing scheme" %>
<% content_for :before_content do %>
<%= govuk_back_link(
text: "Back",
href: :back,
) %>
<% end %>
<%= render partial: "organisations/headings", locals: { main: "Create a new supported housing scheme", sub: nil } %>
<%= form_for(@scheme, method: :patch) do |f| %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= f.govuk_error_summary %>
<%= f.govuk_text_field :service_name,
label: { text: "Scheme name", size: "m" },
hint: { text: "This is how you’ll refer to this supported housing scheme within your organisation. For example, the name could relate to the address or location. You’ll be able to see the client group when selecting it." } %>
<%= f.govuk_check_box :sensitive,
1,
0,
checked: @scheme.sensitive?,
multiple: false,
label: { text: "This scheme contains confidential information" } %>
<% null_option = [OpenStruct.new(id: "", name: "Select an option")] %>
<% organisations = Organisation.all.map { |org| OpenStruct.new(id: org.id, name: org.name) } %>
<% stock_org_answer_options = null_option + organisations %>
<%= f.govuk_collection_select :stock_owning_organisation_id,
stock_org_answer_options,
:id,
:name,
label: { text: "Which organisation owns the housing stock for this scheme?", size: "m" },
"data-controller": %w[accessible-autocomplete conditional-filter] %>
<% if current_user.support? %>
<%= f.govuk_collection_select :organisation_id,
organisations,
:id,
:name,
label: { text: "Which organisation manages this scheme?", size: "m" },
options: { required: true },
"data-controller": %w[accessible-autocomplete conditional-filter] %>
<% end %>
<% if current_user.data_coordinator? %>
<%= f.hidden_field :organisation_id, value: current_user.organisation.id %>
<% end %>
<% scheme_types_selection = Scheme.scheme_types.keys.excluding("Missing").map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
<%= f.govuk_collection_radio_buttons :scheme_type,
scheme_types_selection,
:id,
:name,
legend: { text: "What is this type of scheme?", size: "m" } %>
<% care_acts_selection = Scheme.registered_under_care_acts.keys.reverse.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
<%= f.govuk_collection_radio_buttons :registered_under_care_act,
care_acts_selection,
:id,
:name,
legend: { text: "Is this scheme registered under the Care Standards Act 2000?", size: "m" } %>
<%= f.hidden_field :page, value: "details" %>
<% if request.query_parameters["check_answers"] %>
<%= f.hidden_field :check_answers, value: "true" %>
<% end %>
<%= f.govuk_submit "Save and continue" %>
</div>
</div>
<% end %>

4
app/views/schemes/index.html.erb

@ -7,7 +7,9 @@
<h2 class="govuk-visually-hidden">Supported housing schemes</h2> <h2 class="govuk-visually-hidden">Supported housing schemes</h2>
<%= render SearchComponent.new(current_user:, search_label: "Search by scheme name or code", value: @searched) %> <%= govuk_button_link_to "Create a new supported housing scheme", new_scheme_path, html: { method: :post } %>
<%= render SearchComponent.new(current_user:, search_label: "Search by scheme name, code or postcode", value: @searched) %>
<hr class="govuk-section-break govuk-section-break--visible govuk-section-break--m"> <hr class="govuk-section-break govuk-section-break--visible govuk-section-break--m">

72
app/views/schemes/new.html.erb

@ -0,0 +1,72 @@
<% content_for :title, "Create a new supported housing scheme" %>
<% content_for :before_content do %>
<%= govuk_back_link(
text: "Back",
href: "javascript:history.go(-1);",
) %>
<% end %>
<%= form_for(@scheme, as: :scheme, method: :post) do |f| %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= f.govuk_error_summary %>
<h1 class="govuk-heading-l">
<%= content_for(:title) %>
</h1>
<%= f.govuk_text_field :service_name,
label: { text: "Scheme name", size: "m" },
hint: { text: "This is how you refer to this supported housing scheme within your organisation. For example, the name could relate to the address or location. You’ll be able to see the client group when selecting it." } %>
<%= f.govuk_check_box :sensitive,
"Yes",
checked: @scheme.sensitive?,
multiple: false,
label: { text: "This scheme contains confidential information" } %>
<% null_option = [OpenStruct.new(id: "", name: "Select an option")] %>
<% organisations = Organisation.all.map { |org| OpenStruct.new(id: org.id, name: org.name) } %>
<% answer_options = null_option + organisations %>
<%= f.govuk_collection_select :stock_owning_organisation_id,
answer_options,
:id,
:name,
label: { text: "Which organisation owns the housing stock for this scheme?", size: "m" },
"data-controller": %w[accessible-autocomplete conditional-filter] %>
<% if current_user.support? %>
<%= f.govuk_collection_select :organisation_id,
answer_options,
:id,
:name,
label: { text: "Which organisation manages this scheme?", size: "m" },
"data-controller": %w[accessible-autocomplete conditional-filter] %>
<% end %>
<% if current_user.data_coordinator? %>
<%= f.hidden_field :organisation_id, value: current_user.organisation.id %>
<% end %>
<% scheme_types_selection = Scheme.scheme_types.keys.excluding("Missing").map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
<%= f.govuk_collection_radio_buttons :scheme_type,
scheme_types_selection,
:id,
:name,
legend: { text: "What is this type of scheme?", size: "m" } %>
<% care_acts_selection = Scheme.registered_under_care_acts.keys.reverse.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
<%= f.govuk_collection_radio_buttons :registered_under_care_act,
care_acts_selection,
:id,
:name,
legend: { text: "Is this scheme registered under the Care Standards Act 2000?", size: "m" } %>
<%= f.govuk_submit "Save and continue" %>
</div>
</div>
<% end %>

35
app/views/schemes/primary_client_group.html.erb

@ -0,0 +1,35 @@
<% content_for :title, "What client group is this scheme intended for?" %>
<% content_for :before_content do %>
<%= govuk_back_link(
text: "Back",
href: request.query_parameters["check_answers"] ? "/schemes/#{@scheme.id}/check-answers" : "/schemes/#{@scheme.id}/details",
) %>
<% end %>
<%= render partial: "organisations/headings", locals: { main: "What client group is this scheme intended for?", sub: @scheme.service_name } %>
<%= form_for(@scheme, method: :patch) do |f| %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= f.govuk_error_summary %>
<legend class="govuk-fieldset__legend">
</legend>
<% primary_client_group_selection = Scheme.primary_client_groups.keys.excluding("Missing").map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
<%= f.govuk_collection_radio_buttons :primary_client_group,
primary_client_group_selection,
:id,
:name,
legend: nil %>
<%= f.hidden_field :page, value: "primary-client-group" %>
<% if request.query_parameters["check_answers"] == "true" %>
<%= f.hidden_field :check_answers, value: "true" %>
<% end %>
<%= f.govuk_submit "Save and continue" %>
</div>
</div>
<% end %>

35
app/views/schemes/secondary_client_group.html.erb

@ -0,0 +1,35 @@
<% content_for :title, "What is the other client group?" %>
<% content_for :before_content do %>
<%= govuk_back_link(
text: "Back",
href: request.query_parameters["check_answers"] ? "/schemes/#{@scheme.id}/check-answers" : "/schemes/#{@scheme.id}/confirm-secondary-client-group",
) %>
<% end %>
<%= render partial: "organisations/headings", locals: { main: "What is the other client group?", sub: @scheme.service_name } %>
<%= form_for(@scheme, method: :patch) do |f| %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= f.govuk_error_summary %>
<legend class="govuk-fieldset__legend">
</legend>
<% secondary_client_group_selection = Scheme.secondary_client_groups.keys.excluding("Missing").map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
<%= f.govuk_collection_radio_buttons :secondary_client_group,
secondary_client_group_selection,
:id,
:name,
legend: nil %>
<%= f.hidden_field :page, value: "secondary-client-group" %>
<% if request.query_parameters["check_answers"] == "true" %>
<%= f.hidden_field :check_answers, value: "true" %>
<% end %>
<%= f.govuk_submit "Save and continue" %>
</div>
</div>
<% end %>

39
app/views/schemes/support.html.erb

@ -0,0 +1,39 @@
<% content_for :title, "What support does this scheme provide?" %>
<% content_for :before_content do %>
<%= govuk_back_link(
text: "Back",
href: request.query_parameters["check_answers"] ? "/schemes/#{@scheme.id}/check-answers" : "/schemes/#{@scheme.id}/secondary-client-group",
) %>
<% end %>
<%= render partial: "organisations/headings", locals: { main: "What support does this scheme provide?", sub: @scheme.service_name } %>
<%= form_for(@scheme, method: :patch) do |f| %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= f.govuk_error_summary %>
<legend class="govuk-fieldset__legend">
</legend>
<% support_type_selection = Scheme.support_types.keys.excluding("Missing").map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
<%= f.govuk_collection_radio_buttons :support_type,
support_type_selection,
:id,
:name,
legend: { text: "Level of support given", size: "m" } %>
<% intended_stay_selection = Scheme.intended_stays.keys.excluding("Missing").map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
<%= f.govuk_collection_radio_buttons :intended_stay,
intended_stay_selection,
:id,
:name,
legend: { text: "Intended length of stay", size: "m" } %>
<%= f.hidden_field :page, value: "support" %>
<%= f.govuk_submit "Save and continue" %>
</div>
</div>
<% end %>

2
app/views/start/index.html.erb

@ -19,7 +19,7 @@
<h2 class="govuk-heading-m">Before you start</h2> <h2 class="govuk-heading-m">Before you start</h2>
<p class="govuk-body">Use your account details to sign in.</p> <p class="govuk-body">Use your account details to sign in.</p>
<p class="govuk-body">If you need to set up a new account, speak to your organisation’s CORE data coordinator. If you don’t know who that is, <%= govuk_link_to("contact the helpdesk", "https://digital.dclg.gov.uk/jira/servicedesk/customer/portal/4/group/21") %>.</p> <p class="govuk-body">If you need to set up a new account, speak to your organisation’s CORE data coordinator. If you don’t know who that is, <%= govuk_link_to("contact the helpdesk", "https://digital.dclg.gov.uk/jira/servicedesk/customer/portal/4/group/21") %>.</p>
<p class="govuk-body">You can <%= govuk_link_to("request an account", "https://digital.dclg.gov.uk/jira/servicedesk/customer/portal/4/group/21") %> if your organisation doesn’t have one.</p> <p class="govuk-body">You can <%= govuk_mail_to("dluhc.digital-services@levellingup.gov.uk", "request an account", subject: "CORE: Request a new account") %> if your organisation doesn’t have one.</p>
</div> </div>
<div class="govuk-grid-column-one-third-from-desktop"> <div class="govuk-grid-column-one-third-from-desktop">

10
config/locales/en.yml

@ -38,6 +38,14 @@ en:
create_password: "Create a password to finish setting up your account" create_password: "Create a password to finish setting up your account"
reset_password: "Reset your password" reset_password: "Reset your password"
activerecord:
errors:
models:
scheme:
attributes:
organisation:
required: "Enter the existing organisation’s name"
validations: validations:
organisation: organisation:
name_missing: "Enter the organisation’s name" name_missing: "Enter the organisation’s name"
@ -149,7 +157,7 @@ en:
rent_period: rent_period:
invalid_for_org: "%{org_name} does not charge rent %{rent_period}" invalid_for_org: "%{org_name} does not charge rent %{rent_period}"
carehome: carehome:
over_1000: "Household rent and other charges must be between %{min_chcharge} and %{max_chcharge} if paying %{period}" out_of_range: "Household rent and other charges must be between %{min_chcharge} and %{max_chcharge} if paying %{period}"
not_provided: "Enter how much rent and other charges the household pays %{period}" not_provided: "Enter how much rent and other charges the household pays %{period}"
household: household:
reasonpref: reasonpref:

9
config/routes.rb

@ -35,7 +35,14 @@ Rails.application.routes.draw do
get "edit/password", to: "users#edit_password" get "edit/password", to: "users#edit_password"
end end
resources :schemes, only: %i[index show] do resources :schemes do
get "primary-client-group", to: "schemes#primary_client_group"
get "confirm-secondary-client-group", to: "schemes#confirm_secondary_client_group"
get "secondary-client-group", to: "schemes#secondary_client_group"
get "support", to: "schemes#support"
get "details", to: "schemes#details"
get "check-answers", to: "schemes#check_answers"
member do member do
get "locations", to: "schemes#locations" get "locations", to: "schemes#locations"
end end

2
db/migrate/20220616130451_add_reference_to_case_log.rb

@ -1,5 +1,5 @@
class AddReferenceToCaseLog < ActiveRecord::Migration[7.0] class AddReferenceToCaseLog < ActiveRecord::Migration[7.0]
def change def change
add_reference :case_logs, :scheme, foreign_key: true add_reference :case_logs, :scheme, foreign_key: true, null: true
end end
end end

7
db/migrate/20220623132228_add_has_other_client_group_field.rb

@ -0,0 +1,7 @@
class AddHasOtherClientGroupField < ActiveRecord::Migration[7.0]
def change
change_table :schemes, bulk: true do |t|
t.column :has_other_client_group, :integer
end
end
end

5
db/migrate/20220629100324_remove_code_from_schemes.rb

@ -0,0 +1,5 @@
class RemoveCodeFromSchemes < ActiveRecord::Migration[7.0]
def change
remove_column :schemes, :code, :string
end
end

5
db/migrate/20220629105452_add_stock_owning_organisation_to_schemes.rb

@ -0,0 +1,5 @@
class AddStockOwningOrganisationToSchemes < ActiveRecord::Migration[7.0]
def change
add_reference :schemes, :stock_owning_organisation, foreign_key: { to_table: :organisations }
end
end

5
db/schema.rb

@ -299,7 +299,6 @@ ActiveRecord::Schema[7.0].define(version: 2022_06_30_154441) do
end end
create_table "schemes", force: :cascade do |t| create_table "schemes", force: :cascade do |t|
t.string "code"
t.string "service_name" t.string "service_name"
t.bigint "organisation_id", null: false t.bigint "organisation_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
@ -313,7 +312,10 @@ ActiveRecord::Schema[7.0].define(version: 2022_06_30_154441) do
t.integer "support_type" t.integer "support_type"
t.string "intended_stay" t.string "intended_stay"
t.datetime "end_date" t.datetime "end_date"
t.integer "has_other_client_group"
t.bigint "stock_owning_organisation_id"
t.index ["organisation_id"], name: "index_schemes_on_organisation_id" t.index ["organisation_id"], name: "index_schemes_on_organisation_id"
t.index ["stock_owning_organisation_id"], name: "index_schemes_on_stock_owning_organisation_id"
end end
create_table "users", force: :cascade do |t| create_table "users", force: :cascade do |t|
@ -373,4 +375,5 @@ ActiveRecord::Schema[7.0].define(version: 2022_06_30_154441) do
add_foreign_key "case_logs", "schemes" add_foreign_key "case_logs", "schemes"
add_foreign_key "locations", "schemes" add_foreign_key "locations", "schemes"
add_foreign_key "schemes", "organisations" add_foreign_key "schemes", "organisations"
add_foreign_key "schemes", "organisations", column: "stock_owning_organisation_id"
end end

3
db/seeds.rb

@ -71,7 +71,6 @@ unless Rails.env.test?
if Rails.env.development? && Scheme.count.zero? if Rails.env.development? && Scheme.count.zero?
scheme1 = Scheme.create!( scheme1 = Scheme.create!(
code: "S878",
service_name: "Beulahside Care", service_name: "Beulahside Care",
sensitive: 0, sensitive: 0,
registered_under_care_act: 0, registered_under_care_act: 0,
@ -86,7 +85,6 @@ unless Rails.env.test?
) )
scheme2 = Scheme.create!( scheme2 = Scheme.create!(
code: "S312",
service_name: "Abdullahview Point", service_name: "Abdullahview Point",
sensitive: 0, sensitive: 0,
registered_under_care_act: 1, registered_under_care_act: 1,
@ -101,7 +99,6 @@ unless Rails.env.test?
) )
Scheme.create!( Scheme.create!(
code: "7XYZ",
service_name: "Caspermouth Center", service_name: "Caspermouth Center",
sensitive: 1, sensitive: 1,
registered_under_care_act: 1, registered_under_care_act: 1,

195
docs/developer_setup.md

@ -0,0 +1,195 @@
# Developing locally on host machine
The most common way to run a development version of the application is run with local dependencies.
Dependencies:
- [Ruby](https://www.ruby-lang.org/en/)
- [Rails](https://rubyonrails.org/)
- [PostgreSQL](https://www.postgresql.org/)
- [NodeJS](https://nodejs.org/en/)
- [Gecko driver](https://github.com/mozilla/geckodriver/releases) [for running Selenium tests]
We recommend using [RBenv](https://github.com/rbenv/rbenv) to manage Ruby versions.
1. Install PostgreSQL
macOS:
```bash
brew install postgresql
brew services start postgresql
```
Linux (Debian):
```bash
sudo apt install -y postgresql postgresql-contrib libpq-dev
sudo systemctl start postgresql
```
2. Create a Postgres user
```bash
sudo su - postgres -c "createuser <username> -s -P"
```
3. Install RBenv & Ruby-build
macOS:
```bash
brew install rbenv
rbenv init
mkdir -p ~/.rbenv/plugins
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
```
Linux (Debian):
```bash
sudo apt install -y rbenv git
rbenv init
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
mkdir -p ~/.rbenv/plugins
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
```
4. Install Ruby and Bundler
```bash
rbenv install 3.1.2
rbenv global 3.1.2
source ~/.bashrc
gem install bundler
```
5. Install JavaScript dependencies
macOS:
```bash
brew install node
brew install yarn
```
Linux (Debian):
```bash
curl -sL https://deb.nodesource.com/setup_16.x | sudo bash -
sudo apt -y install nodejs
mkdir -p ~/.npm-packages
npm config set prefix ~/.npm-packages
echo 'NPM_PACKAGES="~/.npm-packages"' >> ~/.bashrc
echo 'export PATH="$PATH:$NPM_PACKAGES/bin"' >> ~/.bashrc
source ~/.bashrc
npm install --location=global yarn
```
6. Clone the repo
```bash
git clone https://github.com/communitiesuk/submit-social-housing-lettings-and-sales-data.git
```
## Application setup
1. Copy the `.env.example` to `.env` and replace the database credentials with your local postgres user credentials.
2. Install the dependencies:
```bash
bundle install && yarn install
```
3. Create the database & run migrations:
```bash
bundle exec rake db:create db:migrate
```
4. Seed the database if required:
```bash
bundle exec rake db:seed
```
5. Start the dev servers
a. Using Foreman:
```bash
./bin/dev
```
b. Individually:
Rails:
```bash
bundle exec rails s
```
JavaScript (for hot reloading):
```bash
yarn build --mode=development --watch
```
If you’re not modifying front end assets you can bundle them as a one off task:
```bash
yarn build --mode=development
```
Development mode will target the latest versions of Chrome, Firefox and Safari for transpilation while production mode will target older browsers.
The Rails server will start on <http://localhost:3000>.
6. Install Gecko Driver
Linux (Debian):
```bash
wget https://github.com/mozilla/geckodriver/releases/download/v0.31.0/geckodriver-v0.31.0-linux64.tar.gz
tar -xvzf geckodriver-v0.31.0-linux64.tar.gz
rm geckodriver-v0.31.0-linux64.tar.gz
chmod +x geckodriver
sudo mv geckodriver /usr/local/bin/
```
Running the test suite (front end assets need to be built or server needs to be running):
```bash
bundle exec rspec
```
## Using Docker
1. Build the image:
```bash
docker-compose build
```
2. Run the database migrations:
```bash
docker-compose run --rm app /bin/bash -c 'rake db:migrate'
```
3. Seed the database if required:
```bash
docker-compose run --rm app /bin/bash -c 'rake db:seed'
```
4. To be able to debug with Pry run the app using:
```bash
docker-compose run --service-ports app
```
If this is not needed you can run `docker-compose up` as normal
The Rails server will start on <http://localhost:8080>.

17
docs/exports.md

@ -0,0 +1,17 @@
# Exporting to CDS
All data collected by the application needs to be exported to the Consolidated Data Store (CDS) which is a data warehouse based on MS SQL running in the DAP (Data Analytics Platform).
This is done via XML exports saved in an S3 bucket located in the DAP VPC using dedicated credentials shared out of band. The data mapping for this export can be found in `app/services/exports/case_log_export_service.rb`
Initially the application database field names and field types were chosen to match the existing CDS data as closely as possible to minimise the amount of transformation needed. This has led to a less than optimal data model though and increasingly we should look to transform at the mapping layer where beneficial for our application.
The export service is triggered nightly using [Gov PaaS tasks](https://docs.cloudfoundry.org/devguide/using-tasks.html). These tasks are triggered from a Github action, as Gov PaaS does not currently support the Cloud Foundry Task Scheduler.
The S3 bucket is located in the DAP VPC rather than the application VPC as DAP runs in an AWS account directly so access to the S3 bucket can be restricted to only the IPs used by the application. This is not possible the other way around as [Gov PaaS does not support restricting S3 access by IP](https://github.com/alphagov/paas-roadmap/issues/107).
## Other options previously considered
- CDC replication using a managed service such as [AWS DMS](https://aws.amazon.com/dms/)
- Would require VPC peering which [Gov PaaS does not currently support](https://github.com/alphagov/paas-roadmap/issues/105)
- Would require CDS to make changes to their ingestion model

190
docs/form_builder.md

@ -0,0 +1,190 @@
# Form Builder
## Background
Social housing lettings and sales data is collected in annual collection windows that run from 1st April to 1st April.
During this window the form and questions generally stay constant. The form will generally change by small amounts between each collection window. Typical changes are adding new questions, adding or removing answer options from questions or tweaking question wording for clarity.
A paper form is produced for guidance and to help data providers collect the data offline, and a bulk upload template is circulated which need to match the online form.
Data is accepted for a collection window for up to 3 months after it’s finished to allow for late data submission. This means that between April and July two version of the form run simultaneously.
Other considerations that went into our design are being able to re-use as much of this solution for other data collections, and possibly having the ability to generate the form and/or form changes from a user interface.
We haven’t used micro-services, preferring to deploy a single application but we have modelled the form itself as configuration in the form of a JSON structure that acts as a sort of DSL/form builder for the form.
The idea is to decouple the code that creates the required routes, controller methods, views etc to display the form from the actual wording of questions or order of pages such that it becomes possible to make changes to the form with little or no code changes.
This should also mean that in the future it could be possible to create an interface that can construct the JSON config, which would open up the ability to make form changes to a wider audience. Doing this fully would require generating and running the necessary migrations for data storage, generating the required ActiveRecord methods to validate the data server side, and generating/updating API endpoints and documentation. All of this is likely to be beyond the scope of initial MVP but could be looked at in the future.
Since initially the JSON config will not create database migrations or ActiveRecord model validations, it will instead assume that these have been correctly created for the config provided. The reasoning for this is the following assumptions:
- The form will be tweaked regularly (amending questions wording, changing the order of questions or the page a question is displayed on)
- The actual data collected will change very infrequently. Time series continuity is very important to ADD (Analysis and Data Directorate) so the actual data collected should stay largely consistent i.e. in general we can change the question wording in ways that makes the intent clearer or easier to understand, but not in ways that would make the data provider give a different answer.
A form parser class will parse this config into ruby objects/methods that can be used as an API by the rest of the application, such that we could change the underlying config if needed (for example swap JSON for YAML or for DataBase objects) without needing to change the rest of the application. We’ll call this the Form Runner part of the application.
## Setup this log
The setup this log section is treated slightly differently from the rest of the form. It is more accurately viewed as providing metadata about the form than as being part of the form itself. It also needs to know far more about the application specific context than other parts of the form such as who the current user is, what organisation they’re part of and what role they have etc.
As a result it’s not modelled as part of the config but rather as code. It still uses the same Form Runner components though.
## Features the Form Config supports
- Defining sections, subsections, pages and questions that fit the GOV.UK task list pattern
- Auto-generated routes – URLs are automatically created from dasherized page names
- Data persistence requires a database field to exist which matches the name/id for each question (and answer option for checkbox questions)
- Text, numeric, date, radio, select and checkbox question types
- Conditional questions (`conditional_for`) – Radio and checkbox questions can support conditional text or numeric questions that show/hide on the same page when the triggering option is selected
- Routing (`depends_on`) – all pages can specify conditions (attributes of the case log) that determine whether or not they’re shown to the user
- Methods can be chained (i.e. you can have conditions in the form `{ owning_organisation.provider_type: "local_authority"`) which will call `case_log.owning_organisation.provider_type` and compare the result to the provided value.
- Numeric questions support math expression depends_on conditions such as `{ age2: ">16" }`
- By default questions on pages that are not routed to are assumed to be invalid and are cleared. This can be prevented by setting `derived: true` on a question.
- Questions can be optionally hidden from the check answers page of each section by setting `hidden_in_check_answers: true`. This can also take a condition.
- Questions can be set as being inferred from other answers. This is similar to derived with the difference being that derived questions can be derived from anything not just other form question answers, and inferred answers are cleared when the answers they depend on change, whereas derived questions aren’t.
- Soft validation interruption pages can be included
- For complex HTML guidance partials can be referenced
## JSON Config
The form for this is driven by a JSON file in `/config/forms/{start_year}_{end_year}.json`
The JSON should follow the structure:
```jsonc
{
"form_type": "lettings" / "sales",
"start_year": Integer, // i.e. 2020
"end_year": Integer, // i.e. 2021
"sections": {
"[snake_case_section_name_string]": {
"label": String,
"description": String,
"subsections": {
"[snake_case_subsection_name_string]": {
"label": String,
"pages": {
"[snake_case_page_name_string]": {
"header": String,
"description": String,
"questions": {
"[snake_case_question_name_string]": {
"header": String,
"hint_text": String,
"check_answer_label": String,
"type": "text" / "numeric" / "radio" / "checkbox" / "date",
"min": Integer, // numeric only
"max": Integer, // numeric only
"step": Integer, // numeric only
"width": 2 / 3 / 4 / 5 / 10 / 20, // text and numeric only
"prefix": String, // numeric only
"suffix": String, //numeric only
"answer_options": { // checkbox and radio only
"0": String,
"1": String
},
"conditional_for": {
"[snake_case_question_to_enable_1_name_string]": ["condition-that-enables"],
"[snake_case_question_to_enable_2_name_string]": ["condition-that-enables"]
},
"inferred_answers": { "field_that_gets_inferred_from_current_field": { "is_that_field_inferred": true } },
"inferred_check_answers_value": {
"condition": { "field_name_for_inferred_check_answers_condition": "field_value_for_inferred_check_answers_condition" },
"value": "Inferred value that gets displayed if condition is met"
}
}
},
"depends_on": [{ "question_key": "answer_value_required_for_this_page_to_be_shown" }]
}
}
}
}
}
}
}
```
Assumptions made by the format:
- All forms have at least 1 section
- All sections have at least 1 subsection
- All subsections have at least 1 page
- All pages have at least 1 question
- The ActiveRecord case log model has a field for each question name (must match). In the case of checkbox questions it must have one field for every answer option (again names must match).
- Text not required by a page/question such as a header or hint text should be passed as an empty string
- For conditionally shown questions, conditions that have been implemented and can be used are:
- Radio question answer option selected matches one of conditional e.g.\
`["answer-options-1-string", "answer-option-3-string"]`
- Numeric question value matches condition e.g. [">2"], ["<7"] or ["== 6"]
- When the top level question is a radio button and the conditional question is a numeric, text or date field then the conditional question is shown inline
- When the conditional question is a radio, checkbox or select field it should be displayed on it’s own page and "depends_on" should be used rather than "conditional_for"
### Page routing
Form navigation works by stepping sequentially through every page defined in the JSON form definition for the given subsection. For every page it checks if it has "depends_on" conditions. If it does, it evaluates them to determine whether that page should be show or not.
In this way we can build up whole branches by having:
```jsonc
"page_1": { "questions": { "question_1: "answer_options": ["A", "B"] } },
"page_2": { "questions": { "question_2: "answer_options": ["C", "D"] }, "depends_on": [{ "question_1": "A" }] },
"page_3": { "questions": { "question_3: "answer_options": ["E", "F"] }, "depends_on": [{ "question_1": "A" }] },
"page_4": { "questions": { "question_4: "answer_options": ["G", "H"] }, "depends_on": [{ "question_1": "B" }] },
```
## JSON form validation against Schema
To validate the form JSON against the schema you can run:
```bash
rake form_definition:validate["config/forms/2021_2022.json"]
```
Note: you may have to escape square brackets in zsh:
```bash
rake form_definition:validate\["config/forms/2021_2022.json"\]
```
This will validate the given form definition against the schema in `config/forms/schema/generic.json`.
You can also run:
```bash
rake form_definition:validate_all
```
This will validate all forms in directories `["config/forms", "spec/fixtures/forms"]`
## Improvements that could be made
- JSON schema definition could be expanded such that we can better automatically validate that a given config is valid and internally consistent
- Generators could parse a given valid JSON form and generate the required database migrations to ensure all the expected fields exist and are of a compatible type
- The parsed form could be visualised using something like GraphViz to help manually verify the coded config meets requirements

21
docs/form_runner.md

@ -0,0 +1,21 @@
# Form Runner
The Form Runner is composed of:
Ruby Classes:
- A singleton form handler that instantiates an instances of each form definition (config file we have) combined with the setup section that is common to all forms. This is created at rails boot time. (`app/models/form_handler.rb`)
- A `Form` class that is the entry point for parsing a form definition and handles most of the associated logic (`app/models/form.rb`)
- `Section`, `Subsection`, `Page` and `Question` classes (`app/models/form/`)
- Setup subsection specific instances (subclasses) of `Section`, `Subsection`, `Pages` and `Questions` (`app/form/setup/`)
ERB templates:
- The page view which is the main view for each form page (`app/views/form/page.html.erb`)
- Partials for each question type (radio, checkbox, select, text, numeric, date) (`app/views/form/`)
- Partials for specific question guidance (`app/views/form/guidance`)
- The check answers page which is the view for the answer summary page of each section (`app/views/form/check_answers.html.erb`)
Routes for each form page are generated by looping over each Page instance in each Form instance held by the form handler and defining a `GET` path. The corresponding controller method is also auto-generated with meta-programming via the same looping in `app/controllers/form_controller.rb`
All form pages submit to the same controller method (`app/controllers/form_controller.rb#submit_form`) which validates and persists the data, and then redirects to the next form page that identifies as `routed_to` given the current case log state.

49
docs/frontend.md

@ -0,0 +1,49 @@
# Frontend
## GOV.UK Design System components
This service follows the guidance and recommendations from the [GOV.UK Design System](https://design-system.service.gov.uk). This is achieved using the following libraries:
- **GOV.UK Frontend** – CSS and JavaScript for all Design System components\
[Documentation](https://frontend.design-system.service.gov.uk) ·
[GitHub](https://github.com/alphagov/govuk-frontend)
- **GOV.UK Components** – Rails view components for non-form related Design System components\
[Documentation](https://govuk-components.netlify.app) ·
[Github](https://github.com/DFE-Digital/govuk-components) ·
[RubyDoc](https://www.rubydoc.info/gems/govuk-components)
- **GOV.UK FormBuilder** Rails form builder for form related Design System components\
[Documentation](https://govuk-form-builder.netlify.app) ·
[GitHub](https://github.com/DFE-Digital/govuk-formbuilder) ·
[RubyDoc](https://www.rubydoc.info/gems/govuk_design_system_formbuilder)
## Service-specific components
Service-specific components are built using the [ViewComponent](https://viewcomponent.org) framework, and can be found in `app/components`.
Components use HTML class names that follow the BEM methodology. We use the `app-*` prefix to prevent collisions with components provided by the Design System (which uses `govuk-*`). See [Extending and modifying components in production](https://design-system.service.gov.uk/get-started/extending-and-modifying-components/).
Stylesheets are written using [Sass](https://sass-lang.com) (and the SCSS syntax), using the mixins and helpers provided by [govuk-frontend](https://frontend.design-system.service.gov.uk/sass-api-reference/).
Separate stylesheets are used for each component, with filenames that match the component’s namespace.
Like the components provided by the Design System, components are progressively enhanced. We use [Stimulus](https://stimulus.hotwired.dev) to add any client-side JavaScript enhancements.
### Stimulus
For adding custom javascript to the application we use [Stimulus](https://stimulus.hotwired.dev/).
The general pattern is:
- Register a controller in `/app/frontend/controllers/index.js`- be sure to use kebab case
- Create that controller in `app/frontend/controllers/` - be sure to use underscore case
- Attach the controller to the html element that should trigger it’s functionality
### Asset bundling and compilation
- We use [Webpack](https://webpack.js.org/) via [jsbundling-rails](https://github.com/rails/jsbundling-rails) to bundle JavaScript, CSS and images. The configuration can be found in `webpack.config.js`.
- We use [Propshaft](https://github.com/rails/propshaft) as our asset pipeline to serve the assets bundled/compiled by webpack
- We use [Babel](https://babeljs.io/) to transpile js down to ES5 for Internet Explorer compatibility. The configuration can be found in `babel.config.js`
- We use [browserslist](https://github.com/browserslist/browserslist) to specify the browsers we want to transpile for. The configuration can be found in `package.json`
- We include a number of polyfills to support Internet Explorer. These can be found in `app/frontend/application.js`

BIN
docs/images/architecture.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
docs/images/organisational_relationships.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
docs/images/service.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

BIN
docs/images/user_log_permissions.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

4
docs/index.html

@ -3,9 +3,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="//pagecdn.io/lib/swagger-ui/v3.31.1/swagger-ui.css"> <link rel="stylesheet" type="text/css" href="//pagecdn.io/lib/swagger-ui/v4.12.0/swagger-ui.css">
<title>OpenAPI DLUHC CORE Data Collection</title> <title>OpenAPI DLUHC CORE Data Collection</title>
<body><div id="openapi"><script src="//pagecdn.io/lib/swagger-ui/v3.31.1/swagger-ui-bundle.js"></script> <body><div id="openapi"><script src="//pagecdn.io/lib/swagger-ui/v4.12.0/swagger-ui-bundle.js"></script>
<script> <script>
window.onload = function () { window.onload = function () {
const ui = SwaggerUIBundle({ const ui = SwaggerUIBundle({

148
docs/infrastructure.md

@ -0,0 +1,148 @@
# Infrastructure
## Deployment
This application is running on [GOV.UK PaaS](https://www.cloud.service.gov.uk/). To deploy you need to:
1. Contact your organisation manager to get an account in `dluhc-core` organization and in the relevant spaces (staging/production).
2. [Install the Cloud Foundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html)
3. Login:
```bash
cf login -a api.london.cloud.service.gov.uk -u <your_username>
```
4. Set your deployment target (staging/production):
```bash
cf target -o dluhc-core -s <deploy_environment>
```
5. Deploy:
```bash
cf push dluhc-core --strategy rolling
```
This will use the [manifest file](staging_manifest.yml)
Once the app is deployed:
1. Get a Rails console:
```bash
cf ssh dluhc-core-staging -t -c "/tmp/lifecycle/launcher /home/vcap/app 'rails console' ''"
```
2. Check logs:
```bash
cf logs dluhc-core-staging --recent
```
### Troubleshooting deployments
A failed Github deployment action will occasionally leave a Cloud Foundry deployment in a broken state. As a result all subsequent Github deployment actions will also fail with the message `Cannot update this process while a deployment is in flight`.
```bash
cf cancel-deployment dluhc-core
```
You would then need to check the logs and fix the issue that caused the initial deployment to fail.
## CI/CD
When a commit is made to `main` the following GitHub action jobs are triggered:
1. **Test**: RSpec runs our test suite
2. **Deploy**: If the Test stage passes, this job will deploy the app to our GOV.UK PaaS account using the Cloud Foundry CLI
When a pull request is opened to `main` only the Test stage runs.
## Setting up Infrastructure for a new environment
### Staging
1. Login:
```bash
cf login -a api.london.cloud.service.gov.uk -u <your_username>
```
2. Set your deployment target (staging):
```bash
cf target -o dluhc-core -s staging
```
3. Create required Postgres and S3 bucket backing services (this will take ~15 mins to finish creating):
```bash
cf create-service postgres tiny-unencrypted-13 dluhc-core-staging-postgres
cf create-service aws-s3-bucket default dluhc-core-staging-import-bucket
cf create-service aws-s3-bucket default dluhc-core-staging-export-bucket
```
4. Deploy manifest:
```bash
cf push dluhc-core-staging --strategy rolling
```
5. Bind S3 services to app:
```bash
cf bind-service dluhc-core-staging dluhc-core-staging-import-bucket -c '{"permissions": "read-only"}'
cf bind-service dluhc-core-staging dluhc-core-staging-export-bucket -c '{"permissions": "read-write"}'
```
6. Create a service keys for accessing the S3 bucket from outside Gov PaaS:
```bash
cf create-service-key dluhc-core-staging-import-bucket data-import -c '{"allow_external_access": true}'
cf create-service-key dluhc-core-staging-export-bucket data-export -c '{"allow_external_access": true, "permissions": "read-only"}'
```
### Production
1. Login:
```bash
cf login -a api.london.cloud.service.gov.uk -u <your_username>
```
2. Set your deployment target (production):
```bash
cf target -o dluhc-core -s production
```
3. Create required Postgres and S3 bucket backing services (this will take ~15 mins to finish creating):
```bash
cf create-service postgres small-ha-13 dluhc-core-production-postgres
cf create-service aws-s3-bucket default dluhc-core-production-import-bucket
cf create-service aws-s3-bucket default dluhc-core-production-export-bucket
```
4. Deploy manifest:
```bash
cf push dluhc-core-production --strategy rolling
```
5. Bind S3 services to app:
```bash
cf bind-service dluhc-core-production dluhc-core-production-import-bucket -c '{"permissions": "read-only"}'
cf bind-service dluhc-core-production dluhc-core-production-export-bucket -c '{"permissions": "read-write"}'
```
6. Create a service keys for accessing the S3 bucket from outside Gov PaaS:
```bash
cf create-service-key dluhc-core-production-import-bucket data-import -c '{"allow_external_access": true}'
cf create-service-key dluhc-core-production-export-bucket data-export -c '{"allow_external_access": true, "permissions": "read-only"}'
```

17
docs/monitoring.md

@ -0,0 +1,17 @@
# Monitoring
We use self-hosted Prometheus and Grafana for monitoring infrastructure metrics. These are run in a dedicated Gov PaaS space called "monitoring" and are deployed as Docker images using Github action pipelines. The repository for these and more information is here: [dluhc-data-collection-monitoring](https://github.com/communitiesuk/dluhc-data-collection-monitoring).
## Performance monitoring and alerting
For application error and performance monitoring we use managed [Sentry](https://sentry.io/organizations/dluhc-core). You will need to be added to the DLUHC account to access this. It triggers slack notifications to the #team-data-collection-alerts channel for all application errors in staging and production and for any controller endpoints that have a P95 transaction duration > 250ms over a 24 hour period.
## Logs
For log persistence we use a managed ELK (Elasticsearch, Logstash, Kibana) stack provided by [Logit](https://logit.io/). You will need to be added to the DLUHC account to access this. Longs are retained for 14 days with a daily limit of 2GB.
Logs are also available from Gov PaaS directly via CLI:
```bash
cf logs <gov-paas-space-name> --recent
```

25
docs/organisations.md

@ -0,0 +1,25 @@
# Organisational relationships
## Definitions
- **Stock owning organisation**: An organisation that owns housing stock. It may manage the allocation of people in and out of their accommodation, or it may contract this out to managing agents.
- **Managing agent**: In scenarios where one organisation owns stock and another organisation is contracted to manage the stock and tenants, the latter organisation is often called a ‘managing agent’. Managing agents are responsible for the allocation of people in and out of the accommodation, and/or responsible for the services provided to support those people in the accommodation (in the case of supported housing).
## Permissions
Organisations that own stock can contract out the management of that stock to another organisation. This relationship is often referred to as a parent/child relationship. This is a useful analogy as a parent can have multiple children, and a child can have many parents. A child organisation can also be a parent, and a parent organisation can also be a child organisation:
![Organisational relationships](images/organisational_relationships.png)
The case logs that a user can see depends on their role:
- Customer support users can access any case log
- Data coordinators can access any case log for which the organisation they work for is ultimately responsible for, meaning they can see logs managed by a child organisation
- Data providers can only access case logs for which their organisation manages (or directly owns)
Taking the relationships from the above diagram, and looking at which logs each user can access:
![User log access permissions](images/user_log_permissions.png)

9
docs/schemes.md

@ -0,0 +1,9 @@
# Supported housing schemes
## Schemes
Groups of similar properties in the same location, intended for similar tenants with the same type of support needs, managed in the same way. As some of the information we need about a new tenancy is the same for all new tenancies in the ‘scheme’, users can set up a ‘scheme’ in the CORE system by completing the information once. In Supported Housing forms, the user just supplies the appropriate scheme. This means providers do not have to complete identical information multiple times in each CORE form. Effectively we model these as templates or predefined answer sets.
## Management groups
Schemes are often managed together as part of a ‘management group’. An organisation may have multiple management groups, and each management group may have multiple schemes. For Supported Housing logs, users must select the management group first, then select scheme.

5
docs/service_overview.md

@ -0,0 +1,5 @@
# Service overview
All lettings and and sales of social housing in England need to be logged with the Department for levelling up, housing and communities (DLUHC). This is done by Local Authorities and Housing Associations, who are the primary users of this service. Data is collected via a form that runs on an annual data collection window basis. Form changes are made annually to add new questions, remove any that are no longer needed, or adjust wording or answer options etc. Each data collection window runs from 1st April to 1st April + an extra 3 months to allow for any late submissions, meaning that between April and July, two collection windows are open simultaneously and logs can be submitted for either.
ADD (Analytics & Data Directorate) statisticians are the other primary users of the service. The data collected is transferred to DLUHCs data warehouse (CDS - consolidated data store), via nightly exports to XML which are transferred to S3 and ingested from there. CDS ingests and transforms the data, ultimately storing it in a MS SQL database and exposing it to analysts and statisticians via Amazon Workspaces.

13
docs/testing.md

@ -0,0 +1,13 @@
# Testing strategy
- We use [RSpec](https://rspec.info/) and [Capybara](https://teamcapybara.github.io/capybara/)
- Capybara is used for our feature tests. These use the Rack driver by default (faster) or the Gecko driver (installation required) when the `js: true` option is passed for a test.
- Capybara is configured to run in headless mode but this can be toggled by commenting out `app/spec/rails_helper.rb#L14`
- Capybara is configured to use Gecko driver for JavaScript tests as Chrome is more commonly used and so naturally more likely to be better tested but this can be switched to Chrome driver by changing `app/spec/rails_helper.rb#L13`
- Feature specs are generally written sparingly as they’re also the slowest, where possible a request spec is preferred as this still tests a large surface area (route, controller, model, view) without the performance impact. They are not suitable for tests that need to run JavaScript or test that a specific set of interaction events that trigger a specific set of requests (with high confidence).
- Test data is created with [FactoryBot](https://github.com/thoughtbot/factory_bot) where ever possible

15
docs/users.md

@ -0,0 +1,15 @@
# User roles
## External users
The primary users of the system are external data providing organisations: Local Authorities and Private Registered Providers (Housing Associations). These have 2 main user types:
- Data coordinators – administrators for their own organisation, can also complete logs
- Data providers – complete the logs
Additionally there are Data Protection Officers (DPO), which for some organisations is a separate role, but in our codebase is modelled as an attribute of the user (i.e. a data coordinator or provider can additionally be a DPO). They are responsible for ensuring the organisation has signed the data sharing agreement.
## Internal users
- Customer support (help desk) – can administrate all organisations
- ADD statisticians – primary consumers of the data collected via CDS/DAP

56
infrastructure_setup.md

@ -1,56 +0,0 @@
# Staging
1. Login:\
`cf login -a api.london.cloud.service.gov.uk -u <your_username>`
2. Set your deployment target (staging):\
`cf target -o dluhc-core -s staging`
3. Create required Postgres and S3 bucket backing services (this will take ~15 mins to finish creating):\
`cf create-service postgres tiny-unencrypted-13 dluhc-core-staging-postgres`
`cf create-service aws-s3-bucket default dluhc-core-staging-import-bucket`
`cf create-service aws-s3-bucket default dluhc-core-staging-export-bucket`
4. Deploy manifest:\
`cf push dluhc-core-staging --strategy rolling`
5. Bind S3 services to app:\
`cf bind-service dluhc-core-staging dluhc-core-staging-import-bucket -c '{"permissions": "read-only"}'`
`cf bind-service dluhc-core-staging dluhc-core-staging-export-bucket -c '{"permissions": "read-write"}'`
6. Create a service keys for accessing the S3 bucket from outside Gov PaaS:\
`cf create-service-key dluhc-core-staging-import-bucket data-import -c '{"allow_external_access": true}'`
`cf create-service-key dluhc-core-staging-export-bucket data-export -c '{"allow_external_access": true, "permissions": "read-only"}'`
# Production
1. Login:\
`cf login -a api.london.cloud.service.gov.uk -u <your_username>`
2. Set your deployment target (production):\
`cf target -o dluhc-core -s production`
3. Create required Postgres and S3 bucket backing services (this will take ~15 mins to finish creating):\
`cf create-service postgres small-ha-13 dluhc-core-production-postgres`
`cf create-service aws-s3-bucket default dluhc-core-production-import-bucket`
`cf create-service aws-s3-bucket default dluhc-core-production-export-bucket`
4. Deploy manifest:\
`cf push dluhc-core-production --strategy rolling`
5. Bind S3 services to app:\
`cf bind-service dluhc-core-production dluhc-core-production-import-bucket -c '{"permissions": "read-only"}'`
`cf bind-service dluhc-core-production dluhc-core-production-export-bucket -c '{"permissions": "read-write"}'`
6. Create a service keys for accessing the S3 bucket from outside Gov PaaS:\
`cf create-service-key dluhc-core-production-import-bucket data-import -c '{"allow_external_access": true}'`
`cf create-service-key dluhc-core-production-export-bucket data-export -c '{"allow_external_access": true, "permissions": "read-only"}'`

4
package.json

@ -20,8 +20,8 @@
"css-loader": "^6.7.1", "css-loader": "^6.7.1",
"custom-event-polyfill": "^1.0.7", "custom-event-polyfill": "^1.0.7",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"govuk-frontend": "4.0.1", "govuk-frontend": "4.2.0",
"govuk-prototype-components": "^0.1.5", "govuk-prototype-components": "^0.4.0",
"html5shiv": "^3.7.3", "html5shiv": "^3.7.3",
"intersection-observer": "^0.12.0", "intersection-observer": "^0.12.0",
"mini-css-extract-plugin": "^2.6.0", "mini-css-extract-plugin": "^2.6.0",

1
spec/components/search_component_spec.rb

@ -3,7 +3,6 @@ require "rails_helper"
RSpec.describe SearchComponent, type: :component do RSpec.describe SearchComponent, type: :component do
let(:current_user) { FactoryBot.create(:user, :support) } let(:current_user) { FactoryBot.create(:user, :support) }
let(:search_label) { "Search by name or email address" } let(:search_label) { "Search by name or email address" }
let(:page) { Capybara::Node::Simple.new(rendered_component) }
let(:value) { nil } let(:value) { nil }
before do before do

1
spec/components/search_result_caption_component_spec.rb

@ -1,7 +1,6 @@
require "rails_helper" require "rails_helper"
RSpec.describe SearchResultCaptionComponent, type: :component do RSpec.describe SearchResultCaptionComponent, type: :component do
let(:page) { Capybara::Node::Simple.new(rendered_component) }
let(:searched) { "search item" } let(:searched) { "search item" }
let(:count) { 2 } let(:count) { 2 }
let(:item_label) { "user" } let(:item_label) { "user" }

2
spec/factories/location.rb

@ -1,7 +1,7 @@
FactoryBot.define do FactoryBot.define do
factory :location do factory :location do
location_code { Faker::Name.initials(number: 10) } location_code { Faker::Name.initials(number: 10) }
postcode { Faker::Address.postcode } postcode { Faker::Address.postcode.delete(" ") }
address_line1 { Faker::Address.street_name } address_line1 { Faker::Address.street_name }
address_line2 { Faker::Address.city } address_line2 { Faker::Address.city }
type_of_unit { Faker::Lorem.word } type_of_unit { Faker::Lorem.word }

1
spec/factories/scheme.rb

@ -1,6 +1,5 @@
FactoryBot.define do FactoryBot.define do
factory :scheme do factory :scheme do
code { Faker::Name.initials(number: 4) }
service_name { Faker::Name.name } service_name { Faker::Name.name }
sensitive { Faker::Number.within(range: 0..1) } sensitive { Faker::Number.within(range: 0..1) }
registered_under_care_act { Faker::Number.within(range: 0..1) } registered_under_care_act { Faker::Number.within(range: 0..1) }

408
spec/features/schemes_spec.rb

@ -3,7 +3,7 @@ require "rails_helper"
RSpec.describe "Schemes scheme Features" do RSpec.describe "Schemes scheme Features" do
context "when viewing list of schemes" do context "when viewing list of schemes" do
context "when I am signed as a coordinator user and there are schemes in the database" do context "when I am signed as a coordinator user and there are schemes in the database" do
let(:user) { FactoryBot.create(:user, :data_coordinator, last_sign_in_at: Time.zone.now) } let!(:user) { FactoryBot.create(:user, :data_coordinator, last_sign_in_at: Time.zone.now) }
let!(:schemes) { FactoryBot.create_list(:scheme, 5, organisation: user.organisation) } let!(:schemes) { FactoryBot.create_list(:scheme, 5, organisation: user.organisation) }
let!(:scheme_to_search) { FactoryBot.create(:scheme, organisation: user.organisation) } let!(:scheme_to_search) { FactoryBot.create(:scheme, organisation: user.organisation) }
@ -25,25 +25,25 @@ RSpec.describe "Schemes scheme Features" do
it "shows list of schemes" do it "shows list of schemes" do
schemes.each do |scheme| schemes.each do |scheme|
expect(page).to have_content(scheme.code) expect(page).to have_content(scheme.id)
end end
end end
context "when I search for a specific scheme" do context "when I search for a specific scheme" do
it "there is a search bar with a message and search button for schemes" do it "there is a search bar with a message and search button for schemes" do
expect(page).to have_field("search") expect(page).to have_field("search")
expect(page).to have_content("Search by scheme name or code") expect(page).to have_content("Search by scheme name, code or postcode")
expect(page).to have_button("Search") expect(page).to have_button("Search")
end end
context "when I fill in search information and press the search button" do context "when I fill in search information and press the search button" do
before do before do
fill_in("search", with: scheme_to_search.code) fill_in("search", with: scheme_to_search.id_to_display)
click_button("Search") click_button("Search")
end end
it "displays scheme matching the scheme code" do it "displays scheme matching the scheme code" do
expect(page).to have_content(scheme_to_search.code) expect(page).to have_content(scheme_to_search.id_to_display)
end end
context "when I want to clear results" do context "when I want to clear results" do
@ -53,9 +53,10 @@ RSpec.describe "Schemes scheme Features" do
it "displays all schemes after I clear the search results" do it "displays all schemes after I clear the search results" do
click_link("Clear search") click_link("Clear search")
expect(page).to have_content(scheme_to_search.code) expect(page).to have_content(scheme_to_search.id)
schemes.each do |scheme| schemes.each do |scheme|
expect(page).to have_content(scheme.code) expect(page).to have_content(scheme.id)
end
end end
end end
end end
@ -87,6 +88,7 @@ RSpec.describe "Schemes scheme Features" do
click_button("Submit") click_button("Submit")
end end
context "when viewing list of schemes" do
it "displays the link to the schemes" do it "displays the link to the schemes" do
expect(page).to have_link("Schemes", href: "/schemes") expect(page).to have_link("Schemes", href: "/schemes")
end end
@ -98,25 +100,25 @@ RSpec.describe "Schemes scheme Features" do
it "shows list of schemes" do it "shows list of schemes" do
schemes.each do |scheme| schemes.each do |scheme|
expect(page).to have_content(scheme.code) expect(page).to have_content(scheme.id)
end end
end end
context "when I search for a specific scheme" do context "when I search for a specific scheme" do
it "there is a search bar with a message and search button for schemes" do it "there is a search bar with a message and search button for schemes" do
expect(page).to have_field("search") expect(page).to have_field("search")
expect(page).to have_content("Search by scheme name or code") expect(page).to have_content("Search by scheme name, code or postcode")
expect(page).to have_button("Search") expect(page).to have_button("Search")
end end
context "when I fill in search information and press the search button" do context "when I fill in search information and press the search button" do
before do before do
fill_in("search", with: scheme_to_search.code) fill_in("search", with: scheme_to_search.id_to_display)
click_button("Search") click_button("Search")
end end
it "displays scheme matching the scheme code" do it "displays scheme matching the scheme code" do
expect(page).to have_content(scheme_to_search.code) expect(page).to have_content(scheme_to_search.id_to_display)
end end
context "when I want to clear results" do context "when I want to clear results" do
@ -126,10 +128,9 @@ RSpec.describe "Schemes scheme Features" do
it "displays all schemes after I clear the search results" do it "displays all schemes after I clear the search results" do
click_link("Clear search") click_link("Clear search")
expect(page).to have_content(scheme_to_search.code) expect(page).to have_content(scheme_to_search.id_to_display)
schemes.each do |scheme| schemes.each do |scheme|
expect(page).to have_content(scheme.code) expect(page).to have_content(scheme.id_to_display)
end
end end
end end
end end
@ -139,28 +140,6 @@ RSpec.describe "Schemes scheme Features" do
end end
context "when viewing individual scheme" do context "when viewing individual scheme" do
context "when I am signed as a support user and there are schemes in the database" do
let(:user) { FactoryBot.create(:user, :support, last_sign_in_at: Time.zone.now) }
let!(:schemes) { FactoryBot.create_list(:scheme, 5) }
let(:notify_client) { instance_double(Notifications::Client) }
let(:confirmation_token) { "MCDH5y6Km-U7CFPgAMVS" }
let(:devise_notify_mailer) { DeviseNotifyMailer.new }
let(:otp) { "999111" }
before do
allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer)
allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client)
allow(Devise).to receive(:friendly_token).and_return(confirmation_token)
allow(notify_client).to receive(:send_email).and_return(true)
allow(SecureRandom).to receive(:random_number).and_return(otp)
visit("/logs")
fill_in("user[email]", with: user.email)
fill_in("user[password]", with: user.password)
click_button("Sign in")
fill_in("code", with: otp)
click_button("Submit")
end
context "when I visit schemes page" do context "when I visit schemes page" do
before do before do
visit("schemes") visit("schemes")
@ -169,7 +148,7 @@ RSpec.describe "Schemes scheme Features" do
it "shows list of links to schemes" do it "shows list of links to schemes" do
schemes.each do |scheme| schemes.each do |scheme|
expect(page).to have_link(scheme.service_name) expect(page).to have_link(scheme.service_name)
expect(page).to have_content(scheme.primary_client_group_display) expect(page).to have_content(scheme.primary_client_group)
end end
end end
@ -181,16 +160,15 @@ RSpec.describe "Schemes scheme Features" do
end end
it "shows me details about the selected scheme" do it "shows me details about the selected scheme" do
expect(page).to have_content(schemes.first.code) expect(page).to have_content(schemes.first.id_to_display)
expect(page).to have_content(schemes.first.service_name) expect(page).to have_content(schemes.first.service_name)
expect(page).to have_content(schemes.first.sensitive_display) expect(page).to have_content(schemes.first.sensitive)
expect(page).to have_content(schemes.first.scheme_type_display) expect(page).to have_content(schemes.first.scheme_type)
expect(page).to have_content(schemes.first.registered_under_care_act_display) expect(page).to have_content(schemes.first.registered_under_care_act)
expect(page).to have_content(schemes.first.total_units) expect(page).to have_content(schemes.first.primary_client_group)
expect(page).to have_content(schemes.first.primary_client_group_display) expect(page).to have_content(schemes.first.secondary_client_group)
expect(page).to have_content(schemes.first.secondary_client_group_display) expect(page).to have_content(schemes.first.support_type)
expect(page).to have_content(schemes.first.support_type_display) expect(page).to have_content(schemes.first.intended_stay)
expect(page).to have_content(schemes.first.intended_stay_display)
end end
context "when I click to go back" do context "when I click to go back" do
@ -203,7 +181,7 @@ RSpec.describe "Schemes scheme Features" do
click_on("Back") click_on("Back")
schemes.each do |scheme| schemes.each do |scheme|
expect(page).to have_link(scheme.service_name) expect(page).to have_link(scheme.service_name)
expect(page).to have_content(scheme.primary_client_group_display) expect(page).to have_content(scheme.primary_client_group)
end end
end end
end end
@ -238,5 +216,341 @@ RSpec.describe "Schemes scheme Features" do
end end
end end
end end
context "when creating a new scheme" do
before do
Scheme.destroy_all
click_link "Schemes", href: "/schemes"
end
it "displays the link to create a new scheme" do
expect(page).to have_current_path("/schemes")
expect(page).to have_link("Create a new supported housing scheme")
end
context "when I press create a new scheme" do
let!(:organisation) { FactoryBot.create(:organisation, name: "FooBar") }
before do
click_link "Create a new supported housing scheme"
end
it "lets me fill in the scheme details" do
expect(page).to have_current_path("/schemes/new")
expect(page).to have_content "Scheme name"
expect(page).to have_content "This scheme contains confidential information"
expect(page).to have_content "Which organisation owns the housing stock for this scheme?"
expect(page).to have_content "Which organisation manages this scheme?"
expect(page).to have_content "What is this type of scheme?"
expect(page).to have_content "Is this scheme registered under the Care Standards Act 2000?"
end
context "when I fill in scheme details and I press save I see primary client group section" do
let(:scheme) { Scheme.first }
before do
fill_in "Scheme name", with: "FooBar"
check "This scheme contains confidential information"
choose "Direct access hostel"
choose "Yes – registered care home providing nursing care"
select organisation.name, from: "scheme-organisation-id-field"
select organisation.name, from: "scheme-stock-owning-organisation-id-field"
click_button "Save and continue"
end
it "lets me fill in the scheme details" do
expect(page).to have_content "What client group is this scheme intended for?"
end
context "when I press the back button" do
before do
click_link "Back"
end
it "lets me fill in the scheme details" do
expect(page).to have_current_path("/schemes/#{scheme.id}/details")
expect(page).to have_content "Scheme name"
expect(page).to have_content "This scheme contains confidential information"
expect(page).to have_content "Which organisation manages this scheme"
expect(page).to have_content "What is this type of scheme?"
expect(page).to have_content "Is this scheme registered under the Care Standards Act 2000?"
end
context "when we amend scheme details" do
it "returns to the primary client group question" do
click_button "Save and continue"
expect(page).to have_current_path("/schemes/#{scheme.id}/primary-client-group")
end
end
end
context "when I select primary client group details" do
before do
choose "Homeless families with support needs"
click_button "Save and continue"
end
it "lets me confirm if I want to select secondary group details" do
expect(page).to have_content "Does this scheme provide for another client group?"
end
context "when I press the back button" do
before do
click_link "Back"
end
it "lets me select the primary client group" do
expect(page).to have_current_path("/schemes/#{scheme.id}/primary-client-group")
expect(page).to have_content "What client group is this scheme intended for?"
end
context "when we amend primary client group" do
it "returns to the confirm secondary client group question" do
click_button "Save and continue"
expect(page).to have_current_path("/schemes/#{scheme.id}/confirm-secondary-client-group")
end
end
end
context "when I confirm the secondary group" do
before do
choose "Yes"
click_button "Save and continue"
end
it "lets me select secondary client group" do
expect(page).to have_content "What is the other client group?"
end
context "when I press the back button" do
before do
click_link "Back"
end
it "lets me confirm the secondary group" do
expect(page).to have_current_path("/schemes/#{scheme.id}/confirm-secondary-client-group")
expect(page).to have_content "Does this scheme provide for another client group?"
end
context "when we amend confirm secondary client" do
it "returns to the secondary client group question" do
click_button "Save and continue"
expect(page).to have_current_path("/schemes/#{scheme.id}/secondary-client-group")
end
end
end
context "when I select the secondary group" do
before do
choose "Homeless families with support needs"
click_button "Save and continue"
end
it "lets me select level of support" do
expect(page).to have_content "What support does this scheme provide?"
end
context "when I press the back button" do
before do
click_link "Back"
end
it "lets me select the secondary group" do
expect(page).to have_current_path("/schemes/#{scheme.id}/secondary-client-group")
expect(page).to have_content "What is the other client group?"
end
context "when we amend secondary client" do
it "returns to the support question" do
click_button "Save and continue"
expect(page).to have_current_path("/schemes/#{scheme.id}/support")
end
end
end
context "when I select the support answers" do
before do
choose "Floating support"
choose "Very short stay"
click_button "Save and continue"
end
it "lets me check my answers" do
expect(page).to have_content "Check your changes before updating this scheme"
end
context "when changing answers" do
it "displays change links" do
assert_selector "a", text: "Change", count: 12
end
context "when changing details" do
before do
click_link("Change", href: "/schemes/#{scheme.id}/details?check_answers=true", match: :first)
end
it "allows changing details questions" do
expect(page).to have_current_path("/schemes/#{scheme.id}/details?check_answers=true")
fill_in "Scheme name", with: "Example"
choose "Direct access hostel"
choose "Yes – registered care home providing nursing care"
click_button "Save and continue"
expect(page).to have_current_path("/schemes/#{scheme.id}/check-answers")
expect(page).to have_content "Example"
expect(page).to have_content "Yes – registered care home providing nursing care"
end
context "when I press the back button" do
before do
click_link "Back"
end
it "lets me select the support answers" do
expect(page).to have_current_path("/schemes/#{scheme.id}/check-answers")
expect(page).to have_content "Check your changes before updating this scheme"
end
end
end
context "when changing primary client group" do
before do
click_link("Change", href: "/schemes/#{scheme.id}/primary-client-group?check_answers=true")
end
it "allows changing primary-client-group question" do
expect(page).to have_current_path("/schemes/#{scheme.id}/primary-client-group?check_answers=true")
choose "Older people with support needs"
click_button "Save and continue"
expect(page).to have_current_path("/schemes/#{scheme.id}/check-answers")
expect(page).to have_content "Older people with support needs"
end
context "when I press the back button" do
before do
click_link "Back"
end
it "lets me select the support answers" do
expect(page).to have_current_path("/schemes/#{scheme.id}/check-answers")
expect(page).to have_content "Check your changes before updating this scheme"
end
end
end
context "when changing confirm secondary group answer" do
before do
click_link("Change", href: "/schemes/#{scheme.id}/confirm-secondary-client-group?check_answers=true")
end
it "allows changing confirm-secondary-client-group question to yes" do
expect(page).to have_current_path("/schemes/#{scheme.id}/confirm-secondary-client-group?check_answers=true")
choose "Yes"
click_button "Save and continue"
expect(page).to have_current_path("/schemes/#{scheme.id}/secondary-client-group?check_answers=true")
choose "People at risk of domestic violence"
click_button "Save and continue"
expect(page).to have_current_path("/schemes/#{scheme.id}/check-answers")
expect(page).to have_content "People at risk of domestic violence"
end
context "when I press the back button" do
before do
click_link "Back"
end
it "lets me select the support answers" do
expect(page).to have_current_path("/schemes/#{scheme.id}/check-answers")
expect(page).to have_content "Check your changes before updating this scheme"
end
end
end
context "when allows changing confirm-secondary-client-group question to no" do
before do
click_link("Change", href: "/schemes/#{scheme.id}/confirm-secondary-client-group?check_answers=true")
end
it "allows changing confirm-secondary-client-group question to no" do
expect(page).to have_current_path("/schemes/#{scheme.id}/confirm-secondary-client-group?check_answers=true")
choose "No"
click_button "Save and continue"
expect(page).to have_current_path("/schemes/#{scheme.id}/check-answers")
expect(page).not_to have_content "Secondary client group"
end
end
context "when changing secondary-client-group question" do
before do
click_link("Change", href: "/schemes/#{scheme.id}/secondary-client-group?check_answers=true")
end
it "allows changing secondary-client-group question" do
expect(page).to have_current_path("/schemes/#{scheme.id}/secondary-client-group?check_answers=true")
choose "People at risk of domestic violence"
click_button "Save and continue"
expect(page).to have_current_path("/schemes/#{scheme.id}/check-answers")
expect(page).to have_content "People at risk of domestic violence"
end
context "when I press the back button" do
before do
click_link "Back"
end
it "lets me select the support answers" do
expect(page).to have_current_path("/schemes/#{scheme.id}/check-answers")
expect(page).to have_content "Check your changes before updating this scheme"
end
end
end
context "when changing support questions" do
before do
click_link("Change", href: "/schemes/#{scheme.id}/support?check_answers=true", match: :first)
end
it "allows changing support questions" do
expect(page).to have_current_path("/schemes/#{scheme.id}/support?check_answers=true")
choose "Resettlement support"
choose "Medium stay"
click_button "Save and continue"
expect(page).to have_current_path("/schemes/#{scheme.id}/check-answers")
expect(page).to have_content "Resettlement support"
expect(page).to have_content "Medium stay"
end
context "when I press the back button" do
before do
click_link "Back"
end
it "lets me select the support answers" do
expect(page).to have_current_path("/schemes/#{scheme.id}/check-answers")
expect(page).to have_content "Check your changes before updating this scheme"
end
end
end
end
end
end
end
end
end
end
end
end end
end end

4
spec/helpers/tab_nav_helper_spec.rb

@ -20,8 +20,8 @@ RSpec.describe TabNavHelper do
end end
describe "#scheme_cell" do describe "#scheme_cell" do
it "returns the scheme link, name and primary user group separated by a newline character" do it "returns the scheme link service name and primary user group separated by a newline character" do
expected_html = "<a class=\"govuk-link\" href=\"/schemes\">#{scheme.service_name}</a>\n<span class=\"govuk-visually-hidden\">Scheme </span><span class=\"govuk-!-font-weight-regular app-!-colour-muted\">#{scheme.primary_client_group_display}</span>" expected_html = "<a class=\"govuk-link\" href=\"/schemes\">#{scheme.service_name}</a>\n<span class=\"govuk-visually-hidden\">Scheme </span><span class=\"govuk-!-font-weight-regular app-!-colour-muted\">#{scheme.primary_client_group}</span>"
expect(scheme_cell(scheme)).to match(expected_html) expect(scheme_cell(scheme)).to match(expected_html)
end end
end end

10
spec/models/organisation_spec.rb

@ -4,7 +4,7 @@ RSpec.describe Organisation, type: :model do
describe "#new" do describe "#new" do
let(:user) { FactoryBot.create(:user) } let(:user) { FactoryBot.create(:user) }
let!(:organisation) { user.organisation } let!(:organisation) { user.organisation }
let!(:scheme) { FactoryBot.create(:scheme, organisation:) } let!(:scheme) { FactoryBot.create(:scheme, organisation:, stock_owning_organisation: organisation) }
it "has expected fields" do it "has expected fields" do
expect(organisation.attribute_names).to include("name", "phone", "provider_type") expect(organisation.attribute_names).to include("name", "phone", "provider_type")
@ -14,8 +14,12 @@ RSpec.describe Organisation, type: :model do
expect(organisation.users.first).to eq(user) expect(organisation.users.first).to eq(user)
end end
it "has schemes" do it "has managed_schemes" do
expect(organisation.schemes.first).to eq(scheme) expect(organisation.managed_schemes.first).to eq(scheme)
end
it "has owned_schemes" do
expect(organisation.owned_schemes.first).to eq(scheme)
end end
it "validates provider_type presence" do it "validates provider_type presence" do

41
spec/models/scheme_spec.rb

@ -11,15 +11,15 @@ RSpec.describe Scheme, type: :model do
describe "scopes" do describe "scopes" do
let!(:scheme_1) { FactoryBot.create(:scheme) } let!(:scheme_1) { FactoryBot.create(:scheme) }
let!(:scheme_2) { FactoryBot.create(:scheme) } let!(:scheme_2) { FactoryBot.create(:scheme) }
let!(:location) { FactoryBot.create(:location, scheme: scheme_1) }
let!(:location_2) { FactoryBot.create(:location, scheme: scheme_2) }
context "when searching by code" do context "when filtering by id" do
it "returns case insensitive matching records" do it "returns case insensitive matching records" do
expect(described_class.search_by_code(scheme_1.code.upcase).count).to eq(1) expect(described_class.filter_by_id(scheme_1.id.to_s).count).to eq(1)
expect(described_class.search_by_code(scheme_1.code.downcase).count).to eq(1) expect(described_class.filter_by_id(scheme_1.id.to_s).first.id).to eq(scheme_1.id)
expect(described_class.search_by_code(scheme_1.code.downcase).first.code).to eq(scheme_1.code) expect(described_class.filter_by_id(scheme_2.id.to_s).count).to eq(1)
expect(described_class.search_by_code(scheme_2.code.upcase).count).to eq(1) expect(described_class.filter_by_id(scheme_2.id.to_s).first.id).to eq(scheme_2.id)
expect(described_class.search_by_code(scheme_2.code.downcase).count).to eq(1)
expect(described_class.search_by_code(scheme_2.code.downcase).first.code).to eq(scheme_2.code)
end end
end end
@ -34,14 +34,27 @@ RSpec.describe Scheme, type: :model do
end end
end end
context "when searching by all searchable field" do context "when searching by postcode" do
it "returns case insensitive matching records" do it "returns case insensitive matching records" do
expect(described_class.search_by(scheme_1.code.upcase).count).to eq(1) expect(described_class.search_by_postcode(location.postcode.upcase).count).to eq(1)
expect(described_class.search_by(scheme_1.code.downcase).count).to eq(1) expect(described_class.search_by_postcode(location.postcode.downcase).count).to eq(1)
expect(described_class.search_by(scheme_1.code.downcase).first.code).to eq(scheme_1.code) expect(described_class.search_by_postcode(location.postcode.downcase).first.locations.first.postcode).to eq(location.postcode)
expect(described_class.search_by_service_name(scheme_2.service_name.upcase).count).to eq(1) expect(described_class.search_by_postcode(location_2.postcode.upcase).count).to eq(1)
expect(described_class.search_by_service_name(scheme_2.service_name.downcase).count).to eq(1) expect(described_class.search_by_postcode(location_2.postcode.downcase).count).to eq(1)
expect(described_class.search_by_service_name(scheme_2.service_name.downcase).first.service_name).to eq(scheme_2.service_name) expect(described_class.search_by_postcode(location_2.postcode.downcase).first.locations.first.postcode).to eq(location_2.postcode)
end
end
context "when searching by all searchable fields" do
it "returns case insensitive matching records" do
expect(described_class.search_by(scheme_1.id.to_s).count).to eq(1)
expect(described_class.search_by(scheme_1.id.to_s).first.id).to eq(scheme_1.id)
expect(described_class.search_by(scheme_2.service_name.upcase).count).to eq(1)
expect(described_class.search_by(scheme_2.service_name.downcase).count).to eq(1)
expect(described_class.search_by(scheme_2.service_name.downcase).first.service_name).to eq(scheme_2.service_name)
expect(described_class.search_by(location.postcode.upcase).count).to eq(1)
expect(described_class.search_by(location.postcode.downcase).count).to eq(1)
expect(described_class.search_by(location.postcode.downcase).first.locations.first.postcode).to eq(location.postcode)
end end
end end
end end

58
spec/models/validations/financial_validations_spec.rb

@ -847,9 +847,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 1001 record.chcharge = 1001
financial_validator.validate_care_home_charges(record) financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"]) expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.over_1000", min_chcharge: "£0", period: "weekly for 52 weeks", max_chcharge: 1000)) .to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 10, period: "weekly for 52 weeks", max_chcharge: 1000))
expect(record.errors["period"]) expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.over_1000", min_chcharge: "£0", period: "weekly for 52 weeks", max_chcharge: 1000)) .to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 10, period: "weekly for 52 weeks", max_chcharge: 1000))
end end
it "validates charge when period is monthly" do it "validates charge when period is monthly" do
@ -857,9 +857,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 4334 record.chcharge = 4334
financial_validator.validate_care_home_charges(record) financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"]) expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.over_1000", min_chcharge: "£0", period: "4", max_chcharge: 4333)) .to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "4", max_chcharge: 4333))
expect(record.errors["period"]) expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.over_1000", min_chcharge: "£0", period: "4", max_chcharge: 4333)) .to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "4", max_chcharge: 4333))
end end
it "validates charge when period is every 2 weeks" do it "validates charge when period is every 2 weeks" do
@ -867,9 +867,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 2001 record.chcharge = 2001
financial_validator.validate_care_home_charges(record) financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"]) expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.over_1000", min_chcharge: "£0", period: "2", max_chcharge: 2000)) .to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "2", max_chcharge: 2000))
expect(record.errors["period"]) expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.over_1000", min_chcharge: "£0", period: "2", max_chcharge: 2000)) .to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "2", max_chcharge: 2000))
end end
it "validates charge when period is every 4 weeks" do it "validates charge when period is every 4 weeks" do
@ -877,9 +877,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 4001 record.chcharge = 4001
financial_validator.validate_care_home_charges(record) financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"]) expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.over_1000", min_chcharge: "£0", period: "every 4 weeks", max_chcharge: 4000)) .to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 40, period: "every 4 weeks", max_chcharge: 4000))
expect(record.errors["period"]) expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.over_1000", min_chcharge: "£0", period: "every 4 weeks", max_chcharge: 4000)) .to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 40, period: "every 4 weeks", max_chcharge: 4000))
end end
end end
@ -926,6 +926,48 @@ RSpec.describe Validations::FinancialValidations do
.to include(match I18n.t("validations.financial.carehome.not_provided", period: "every 4 weeks")) .to include(match I18n.t("validations.financial.carehome.not_provided", period: "every 4 weeks"))
end end
end end
context "and charges under valid limit (£10pw)" do
it "validates charge when period is weekly for 52 weeks" do
record.period = 1
record.chcharge = 9
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 10, period: "weekly for 52 weeks", max_chcharge: 1000))
expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 10, period: "weekly for 52 weeks", max_chcharge: 1000))
end
it "validates charge when period is monthly" do
record.period = 4
record.chcharge = 42
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "4", max_chcharge: 4333))
expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "4", max_chcharge: 4333))
end
it "validates charge when period is every 2 weeks" do
record.period = 2
record.chcharge = 19
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "2", max_chcharge: 2000))
expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "2", max_chcharge: 2000))
end
it "validates charge when period is every 4 weeks" do
record.period = 3
record.chcharge = 39
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 40, period: "every 4 weeks", max_chcharge: 4000))
expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 40, period: "every 4 weeks", max_chcharge: 4000))
end
end
end end
end end
end end

4
spec/requests/case_logs_controller_spec.rb

@ -548,7 +548,7 @@ RSpec.describe CaseLogsController, type: :request do
end end
it "has pagination links" do it "has pagination links" do
expect(page).to have_content("Previous") expect(page).not_to have_content("Previous")
expect(page).not_to have_link("Previous") expect(page).not_to have_link("Previous")
expect(page).to have_content("Next") expect(page).to have_content("Next")
expect(page).to have_link("Next") expect(page).to have_link("Next")
@ -575,7 +575,7 @@ RSpec.describe CaseLogsController, type: :request do
it "has pagination links" do it "has pagination links" do
expect(page).to have_content("Previous") expect(page).to have_content("Previous")
expect(page).to have_link("Previous") expect(page).to have_link("Previous")
expect(page).to have_content("Next") expect(page).not_to have_content("Next")
expect(page).not_to have_link("Next") expect(page).not_to have_link("Next")
end end

30
spec/requests/organisations_controller_spec.rb

@ -65,25 +65,26 @@ RSpec.describe OrganisationsController, type: :request do
end end
it "shows only schemes belonging to the same organisation" do it "shows only schemes belonging to the same organisation" do
expect(page).to have_content(same_org_scheme.code) expect(page).to have_content(same_org_scheme.id_to_display)
schemes.each do |scheme| schemes.each do |scheme|
expect(page).not_to have_content(scheme.code) expect(page).not_to have_content(scheme.id_to_display)
end end
end end
context "when searching" do context "when searching" do
let!(:searched_scheme) { FactoryBot.create(:scheme, code: "CODE321", organisation: user.organisation) } let!(:searched_scheme) { FactoryBot.create(:scheme, organisation: user.organisation) }
let(:search_param) { "CODE321" } let(:search_param) { searched_scheme.id }
before do before do
FactoryBot.create(:location, scheme: searched_scheme)
allow(user).to receive(:need_two_factor_authentication?).and_return(false) allow(user).to receive(:need_two_factor_authentication?).and_return(false)
get "/organisations/#{organisation.id}/schemes?search=#{search_param}" get "/organisations/#{organisation.id}/schemes?search=#{search_param}"
end end
it "returns matching results" do it "returns matching results" do
expect(page).to have_content(searched_scheme.code) expect(page).to have_content(searched_scheme.id_to_display)
schemes.each do |scheme| schemes.each do |scheme|
expect(page).not_to have_content(scheme.code) expect(page).not_to have_content(scheme.id_to_display)
end end
end end
@ -121,9 +122,9 @@ RSpec.describe OrganisationsController, type: :request do
end end
it "shows only schemes belonging to the same organisation" do it "shows only schemes belonging to the same organisation" do
expect(page).to have_content(same_org_scheme.code) expect(page).to have_content(same_org_scheme.id_to_display)
schemes.each do |scheme| schemes.each do |scheme|
expect(page).not_to have_content(scheme.code) expect(page).not_to have_content(scheme.id_to_display)
end end
end end
@ -140,17 +141,18 @@ RSpec.describe OrganisationsController, type: :request do
end end
context "when searching" do context "when searching" do
let!(:searched_scheme) { FactoryBot.create(:scheme, code: "CODE321", organisation: user.organisation) } let!(:searched_scheme) { FactoryBot.create(:scheme, organisation: user.organisation) }
let(:search_param) { "CODE321" } let(:search_param) { searched_scheme.id_to_display }
before do before do
FactoryBot.create(:location, scheme: searched_scheme)
get "/organisations/#{organisation.id}/schemes?search=#{search_param}" get "/organisations/#{organisation.id}/schemes?search=#{search_param}"
end end
it "returns matching results" do it "returns matching results" do
expect(page).to have_content(searched_scheme.code) expect(page).to have_content(searched_scheme.id_to_display)
schemes.each do |scheme| schemes.each do |scheme|
expect(page).not_to have_content(scheme.code) expect(page).not_to have_content(scheme.id_to_display)
end end
end end
@ -874,7 +876,7 @@ RSpec.describe OrganisationsController, type: :request do
context "when on the first page" do context "when on the first page" do
it "has pagination links" do it "has pagination links" do
expect(page).to have_content("Previous") expect(page).not_to have_content("Previous")
expect(page).not_to have_link("Previous") expect(page).not_to have_link("Previous")
expect(page).to have_content("Next") expect(page).to have_content("Next")
expect(page).to have_link("Next") expect(page).to have_link("Next")
@ -901,7 +903,7 @@ RSpec.describe OrganisationsController, type: :request do
it "has pagination links" do it "has pagination links" do
expect(page).to have_content("Previous") expect(page).to have_content("Previous")
expect(page).to have_link("Previous") expect(page).to have_link("Previous")
expect(page).to have_content("Next") expect(page).not_to have_content("Next")
expect(page).not_to have_link("Next") expect(page).not_to have_link("Next")
end end

1041
spec/requests/schemes_controller_spec.rb

File diff suppressed because it is too large Load Diff

2094
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save