Browse Source

Merge branch 'main' into CLDC-2041-multiple-errors

pull/2840/head
Rachael Booth 6 months ago committed by GitHub
parent
commit
efcc1e0921
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 22
      .github/workflows/aws_deploy.yml
  2. 6
      .github/workflows/review_teardown_pipeline.yml
  3. 411
      .github/workflows/run_tests.yml
  4. 351
      .github/workflows/staging_pipeline.yml
  5. 10
      Gemfile
  6. 253
      Gemfile.lock
  7. 2
      app/components/bulk_upload_error_row_component.html.erb
  8. 22
      app/components/primary_navigation_component.html.erb
  9. 2
      app/components/search_component.html.erb
  10. 2
      app/components/search_result_caption_component.html.erb
  11. 23
      app/controllers/bulk_upload_lettings_logs_controller.rb
  12. 19
      app/controllers/bulk_upload_sales_logs_controller.rb
  13. 49
      app/controllers/form_controller.rb
  14. 2
      app/controllers/logs_controller.rb
  15. 1
      app/controllers/organisations_controller.rb
  16. 4
      app/controllers/schemes_controller.rb
  17. 12
      app/controllers/sessions_controller.rb
  18. 69
      app/frontend/styles/_primary-navigation.scss
  19. 42
      app/frontend/styles/application.scss
  20. 4
      app/helpers/filters_helper.rb
  21. 4
      app/helpers/form_page_error_helper.rb
  22. 2
      app/helpers/logs_helper.rb
  23. 18
      app/helpers/schemes_helper.rb
  24. 21
      app/mailers/bulk_upload_mailer.rb
  25. 6
      app/models/bulk_upload.rb
  26. 2
      app/models/csv_download.rb
  27. 9
      app/models/derived_variables/lettings_log_variables.rb
  28. 12
      app/models/derived_variables/sales_log_variables.rb
  29. 3
      app/models/export.rb
  30. 1
      app/models/form/lettings/questions/offered.rb
  31. 2
      app/models/form/sales/pages/about_staircase.rb
  32. 2
      app/models/form/sales/pages/equity.rb
  33. 17
      app/models/form/sales/pages/monthly_rent_staircasing.rb
  34. 17
      app/models/form/sales/pages/monthly_rent_staircasing_owned.rb
  35. 2
      app/models/form/sales/pages/owning_organisation.rb
  36. 15
      app/models/form/sales/pages/staircase_first_time.rb
  37. 16
      app/models/form/sales/pages/staircase_initial_date.rb
  38. 18
      app/models/form/sales/pages/staircase_previous.rb
  39. 17
      app/models/form/sales/pages/staircase_sale.rb
  40. 2
      app/models/form/sales/pages/value_shared_ownership.rb
  41. 2
      app/models/form/sales/questions/buyer2_income_known.rb
  42. 6
      app/models/form/sales/questions/buyer2_working_situation.rb
  43. 1
      app/models/form/sales/questions/equity.rb
  44. 15
      app/models/form/sales/questions/monthly_rent_after_staircasing.rb
  45. 15
      app/models/form/sales/questions/monthly_rent_before_staircasing.rb
  46. 2
      app/models/form/sales/questions/mortgageused.rb
  47. 15
      app/models/form/sales/questions/staircase_count.rb
  48. 16
      app/models/form/sales/questions/staircase_first_time.rb
  49. 11
      app/models/form/sales/questions/staircase_initial_date.rb
  50. 11
      app/models/form/sales/questions/staircase_last_date.rb
  51. 2
      app/models/form/sales/questions/staircase_sale.rb
  52. 1
      app/models/form/sales/questions/value.rb
  53. 16
      app/models/form/sales/sections/sale_information.rb
  54. 5
      app/models/form/sales/subsections/shared_ownership_initial_purchase.rb
  55. 4
      app/models/form/sales/subsections/shared_ownership_scheme.rb
  56. 35
      app/models/form/sales/subsections/shared_ownership_staircasing_transaction.rb
  57. 39
      app/models/forms/bulk_upload_lettings/needstype.rb
  58. 2
      app/models/forms/bulk_upload_lettings_resume/fix_choice.rb
  59. 2
      app/models/forms/bulk_upload_sales_resume/fix_choice.rb
  60. 10
      app/models/location.rb
  61. 22
      app/models/log.rb
  62. 4
      app/models/merge_request.rb
  63. 3
      app/models/organisation.rb
  64. 4
      app/models/sales_log.rb
  65. 20
      app/models/scheme.rb
  66. 2
      app/models/user.rb
  67. 2
      app/models/validations/property_validations.rb
  68. 2
      app/models/validations/sales/property_validations.rb
  69. 25
      app/models/validations/sales/sale_information_validations.rb
  70. 2
      app/services/bulk_upload/lettings/log_creator.rb
  71. 14
      app/services/bulk_upload/lettings/validator.rb
  72. 20
      app/services/bulk_upload/lettings/year2024/row_parser.rb
  73. 40
      app/services/bulk_upload/processor.rb
  74. 2
      app/services/bulk_upload/sales/log_creator.rb
  75. 12
      app/services/bulk_upload/sales/validator.rb
  76. 20
      app/services/bulk_upload/sales/year2024/row_parser.rb
  77. 10
      app/services/exports/export_service.rb
  78. 22
      app/services/exports/lettings_log_export_service.rb
  79. 12
      app/services/exports/organisation_export_service.rb
  80. 12
      app/services/exports/user_export_service.rb
  81. 20
      app/services/exports/xml_export_service.rb
  82. 23
      app/views/bulk_upload_lettings_logs/forms/needstype.erb
  83. 21
      app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb
  84. 2
      app/views/bulk_upload_lettings_resume/deletion_report.html.erb
  85. 12
      app/views/bulk_upload_lettings_resume/fix_choice.html.erb
  86. 2
      app/views/bulk_upload_lettings_soft_validations_check/confirm.html.erb
  87. 19
      app/views/bulk_upload_sales_logs/forms/prepare_your_file_2024.html.erb
  88. 2
      app/views/bulk_upload_sales_resume/deletion_report.html.erb
  89. 14
      app/views/bulk_upload_sales_resume/fix_choice.html.erb
  90. 2
      app/views/bulk_upload_sales_soft_validations_check/confirm.html.erb
  91. 4
      app/views/bulk_upload_shared/_moved_user_banner.html.erb
  92. 9
      app/views/bulk_upload_shared/guidance.html.erb
  93. 2
      app/views/bulk_upload_shared/uploads.html.erb
  94. 6
      app/views/cookies/show.html.erb
  95. 6
      app/views/devise/sessions/new.html.erb
  96. 2
      app/views/devise/shared/_links.html.erb
  97. 5
      app/views/form/_checkbox_question.html.erb
  98. 8
      app/views/form/_radio_question.html.erb
  99. 3
      app/views/form/page.html.erb
  100. 2
      app/views/layouts/application.html.erb
  101. Some files were not shown because too many files have changed in this diff Show More

22
.github/workflows/aws_deploy.yml

@ -41,19 +41,17 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: ${{ env.app_repo_role }}
- name: Login to Amazon ECR
id: ecr-login
uses: aws-actions/amazon-ecr-login@v1
with:
mask-password: "true"
uses: aws-actions/amazon-ecr-login@v2
- name: Check if image with tag already exists
run: |
@ -81,16 +79,14 @@ jobs:
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: ${{ env.app_repo_role }}
- name: Login to Amazon ECR
id: ecr-login
uses: aws-actions/amazon-ecr-login@v1
with:
mask-password: "true"
uses: aws-actions/amazon-ecr-login@v2
- name: Get timestamp
id: timestamp
@ -112,7 +108,7 @@ jobs:
echo "image=$registry/$repository:$readable_tag" >> $GITHUB_ENV
- name: Configure AWS credentials for environment
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: arn:aws:iam::${{ inputs.aws_account_id }}:role/${{ inputs.aws_role_prefix }}-deployment
@ -133,7 +129,7 @@ jobs:
image: ${{ env.image }}
- name: Update ad hoc task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.ad-hoc-task-def.outputs.task-definition }}
@ -185,7 +181,7 @@ jobs:
image: ${{ env.image }}
- name: Deploy updated application
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
cluster: ${{ inputs.aws_task_prefix }}-app
service: ${{ inputs.aws_task_prefix }}-app
@ -207,7 +203,7 @@ jobs:
image: ${{ env.image }}
- name: Deploy updated sidekiq
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
cluster: ${{ inputs.aws_task_prefix }}-app
service: ${{ inputs.aws_task_prefix }}-sidekiq

6
.github/workflows/review_teardown_pipeline.yml

@ -25,13 +25,13 @@ jobs:
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: ${{ env.app_repo_role }}
- name: Configure AWS credentials for review environment
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/${{ env.aws_role_prefix }}-deployment
@ -46,7 +46,7 @@ jobs:
network=$(aws ecs describe-services --cluster $cluster --services $service --query services[0].networkConfiguration)
overrides='{ "containerOverrides" : [{ "name" : "app", "command" : ["bundle", "exec", "rake", "db:drop"]}]}'
arn=$(aws ecs run-task --cluster $cluster --task-definition $ad_hoc_task_definition --network-configuration "$network" --overrides "$overrides" --group migrations --launch-type FARGATE --query tasks[0].taskArn)
echo "Waiting for db prepare task to complete"
echo "Waiting for db drop task to complete"
temp=${arn##*/}
id=${temp%*\"}
aws ecs wait tasks-stopped --cluster $cluster --tasks $id

411
.github/workflows/run_tests.yml

@ -0,0 +1,411 @@
name: Run Tests
on:
workflow_call:
pull_request:
types:
- opened
- synchronize
merge_group:
workflow_dispatch:
defaults:
run:
shell: bash
jobs:
test:
name: Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rake parallel:spec['spec\/(?!features|models|requests|services)']
feature_test:
name: Feature Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake db:prepare
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/features --fail-fast --exclude-pattern "spec/features/accessibility_spec.rb"
model_test:
name: Model tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake db:prepare
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/models --fail-fast
requests_test:
name: Requests tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rake parallel:spec['spec/requests']
services_test:
name: Services Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rake parallel:spec['spec\/services']
accessibility_test:
name: Accessibility tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/features/accessibility_spec.rb --fail-fast
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
cache: yarn
node-version: 20
- name: Install packages and symlink local dependencies
run: |
yarn install --immutable --immutable-cache --check-cache
- name: Lint
run: |
bundle exec rake lint
audit:
name: Audit dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Audit
run: |
bundle exec bundler-audit

351
.github/workflows/staging_pipeline.yml

@ -4,11 +4,6 @@ on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
merge_group:
workflow_dispatch:
defaults:
@ -21,347 +16,13 @@ env:
repository: core
jobs:
test:
name: Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rake parallel:spec['spec\/(?!features|models|requests)']
feature_test:
name: Feature Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake db:prepare
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/features --fail-fast --exclude-pattern "spec/features/accessibility_spec.rb"
model_test:
name: Model tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake db:prepare
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/models --fail-fast
requests_test:
name: Requests tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rake parallel:spec['spec/requests']
accessibility_test:
name: Accessibility tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.5
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: data_collector
ports:
- 5432:5432
# Needed because the Postgres container does not provide a health check
# tmpfs makes database faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
GEMFILE_RUBY_VERSION: 3.1.1
DB_HOST: localhost
DB_DATABASE: data_collector
DB_USERNAME: postgres
DB_PASSWORD: password
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
PARALLEL_TEST_PROCESSORS: 4
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Create database
run: |
bundle exec rake parallel:setup
- name: Compile assets
run: |
bundle exec rake assets:precompile
- name: Run tests
run: |
bundle exec rspec spec/features/accessibility_spec.rb --fail-fast
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version: 20
- name: Install packages and symlink local dependencies
run: |
yarn install --immutable --immutable-cache --check-cache
- name: Lint
run: |
bundle exec rake lint
audit:
name: Audit dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Audit
run: |
bundle exec bundler-audit
tests:
name: Run Tests
uses: ./.github/workflows/run_tests.yml
aws_deploy:
name: AWS Deploy
if: github.ref == 'refs/heads/main'
needs: [lint, test, feature_test, requests_test, model_test, audit]
needs: [tests]
uses: ./.github/workflows/aws_deploy.yml
with:
aws_account_id: 107155005276
@ -379,13 +40,13 @@ jobs:
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.aws_region }}
role-to-assume: ${{ env.app_repo_role }}
- name: Configure AWS credentials for the environment
uses: aws-actions/configure-aws-credentials@v3
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: eu-west-2
role-to-assume: arn:aws:iam::107155005276:role/core-staging-deployment

10
Gemfile

@ -6,11 +6,11 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.1.4"
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
gem "rails", "~> 7.0.8.5"
gem "rails", "~> 7.2.2"
# Use postgresql as the database for Active Record
gem "pg", "~> 1.1"
# Use Puma as the app server
gem "puma", "~> 5.6"
gem "puma", "~> 6.4"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails]
@ -18,9 +18,9 @@ gem "jsbundling-rails"
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", ">= 1.4.4", require: false
# GOV UK frontend components
gem "govuk-components", "~> 5.1"
gem "govuk-components", "~> 5.7"
# GOV UK component form builder DSL
gem "govuk_design_system_formbuilder", "~> 5.0"
gem "govuk_design_system_formbuilder", "~> 5.7"
# Convert Markdown into GOV.UK frontend-styled HTML
gem "govuk_markdown"
gem "redcarpet", "~> 3.6"
@ -44,7 +44,7 @@ gem "view_component", "~> 3.9"
# Use the AWS S3 SDK as storage mechanism
gem "aws-sdk-s3"
# Track changes to models for auditing or versioning.
gem "paper_trail"
gem "paper_trail", "~> 15.2"
# Store active record objects in version whodunnits
gem "paper_trail-globalid"

253
Gemfile.lock

@ -1,75 +1,81 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.8.5)
actionpack (= 7.0.8.5)
activesupport (= 7.0.8.5)
actioncable (7.2.2.1)
actionpack (= 7.2.2.1)
activesupport (= 7.2.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.8.5)
actionpack (= 7.0.8.5)
activejob (= 7.0.8.5)
activerecord (= 7.0.8.5)
activestorage (= 7.0.8.5)
activesupport (= 7.0.8.5)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.8.5)
actionpack (= 7.0.8.5)
actionview (= 7.0.8.5)
activejob (= 7.0.8.5)
activesupport (= 7.0.8.5)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.8.5)
actionview (= 7.0.8.5)
activesupport (= 7.0.8.5)
rack (~> 2.0, >= 2.2.4)
zeitwerk (~> 2.6)
actionmailbox (7.2.2.1)
actionpack (= 7.2.2.1)
activejob (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
mail (>= 2.8.0)
actionmailer (7.2.2.1)
actionpack (= 7.2.2.1)
actionview (= 7.2.2.1)
activejob (= 7.2.2.1)
activesupport (= 7.2.2.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.2.1)
actionview (= 7.2.2.1)
activesupport (= 7.2.2.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.2)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.8.5)
actionpack (= 7.0.8.5)
activerecord (= 7.0.8.5)
activestorage (= 7.0.8.5)
activesupport (= 7.0.8.5)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.2.1)
actionpack (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.8.5)
activesupport (= 7.0.8.5)
actionview (7.2.2.1)
activesupport (= 7.2.2.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.8.5)
activesupport (= 7.0.8.5)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.2.2.1)
activesupport (= 7.2.2.1)
globalid (>= 0.3.6)
activemodel (7.0.8.5)
activesupport (= 7.0.8.5)
activemodel-serializers-xml (1.0.2)
activemodel (> 5.x)
activesupport (> 5.x)
activemodel (7.2.2.1)
activesupport (= 7.2.2.1)
activemodel-serializers-xml (1.0.3)
activemodel (>= 5.0.0.a)
activesupport (>= 5.0.0.a)
builder (~> 3.1)
activerecord (7.0.8.5)
activemodel (= 7.0.8.5)
activesupport (= 7.0.8.5)
activestorage (7.0.8.5)
actionpack (= 7.0.8.5)
activejob (= 7.0.8.5)
activerecord (= 7.0.8.5)
activesupport (= 7.0.8.5)
activerecord (7.2.2.1)
activemodel (= 7.2.2.1)
activesupport (= 7.2.2.1)
timeout (>= 0.4.0)
activestorage (7.2.2.1)
actionpack (= 7.2.2.1)
activejob (= 7.2.2.1)
activerecord (= 7.2.2.1)
activesupport (= 7.2.2.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.8.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
activesupport (7.2.2.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
tzinfo (~> 2.0)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
@ -104,6 +110,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
base64 (0.2.0)
bcrypt (3.1.20)
benchmark (0.4.0)
better_html (2.0.2)
actionview (>= 6.0)
activesupport (>= 6.0)
@ -111,7 +118,7 @@ GEM
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (3.1.6)
bigdecimal (3.1.8)
bindex (0.8.1)
bootsnap (1.18.3)
msgpack (~> 1.2)
@ -149,7 +156,8 @@ GEM
crass (1.0.6)
cssbundling-rails (1.4.0)
railties (>= 6.0.0)
date (3.3.4)
csv (3.3.0)
date (3.4.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (4.9.3)
@ -170,6 +178,7 @@ GEM
dotenv-rails (3.0.2)
dotenv (= 3.0.2)
railties (>= 6.1)
drb (2.2.1)
dumb_delegator (1.0.0)
encryptor (3.0.0)
erb_lint (0.5.0)
@ -184,10 +193,10 @@ GEM
tzinfo
event_stream_parser (1.0.0)
excon (0.111.0)
factory_bot (6.4.6)
factory_bot (6.5.0)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
factory_bot_rails (6.4.4)
factory_bot (~> 6.5)
railties (>= 5.0.0)
faker (3.2.3)
i18n (>= 1.8.11, < 2)
@ -203,11 +212,11 @@ GEM
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
govuk-components (5.2.1)
govuk-components (5.7.0)
html-attributes-utils (~> 1.0.0, >= 1.0.0)
pagy (~> 6.0)
view_component (>= 3.9, < 3.11)
govuk_design_system_formbuilder (5.2.0)
pagy (>= 6, < 10)
view_component (>= 3.9, < 3.17)
govuk_design_system_formbuilder (5.7.1)
actionview (>= 6.1)
activemodel (>= 6.1)
activesupport (>= 6.1)
@ -222,6 +231,10 @@ GEM
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
iniparse (1.5.0)
io-console (0.8.0)
irb (1.14.1)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
jsbundling-rails (1.3.0)
railties (>= 6.0.0)
@ -246,7 +259,8 @@ GEM
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.22.0)
logger (1.6.2)
loofah (2.23.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@ -258,13 +272,13 @@ GEM
matrix (0.4.2)
method_source (1.1.0)
mini_mime (1.1.5)
minitest (5.25.1)
minitest (5.25.4)
msgpack (1.7.2)
multipart-post (2.4.1)
nested_form (0.3.2)
net-http (0.4.1)
uri
net-imap (0.4.17)
net-imap (0.5.1)
date
net-protocol
net-pop (0.1.2)
@ -273,12 +287,12 @@ GEM
timeout
net-smtp (0.5.0)
net-protocol
nio4r (2.7.3)
nokogiri (1.16.7-arm64-darwin)
nio4r (2.7.4)
nokogiri (1.17.1-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-darwin)
nokogiri (1.17.1-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux)
nokogiri (1.17.1-x86_64-linux)
racc (~> 1.4)
notifications-ruby-client (6.0.0)
jwt (>= 1.5, < 3)
@ -287,8 +301,8 @@ GEM
childprocess (>= 0.6.3, < 6)
iniparse (~> 1.4)
rexml (~> 3.2)
pagy (6.5.0)
paper_trail (15.1.0)
pagy (9.3.2)
paper_trail (15.2.0)
activerecord (>= 6.1)
request_store (~> 1.4)
paper_trail-globalid (0.2.0)
@ -313,66 +327,79 @@ GEM
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
psych (5.2.1)
date
stringio
public_suffix (5.0.4)
puma (5.6.9)
puma (6.5.0)
nio4r (~> 2.0)
pundit (2.3.1)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (2.2.10)
rack (3.1.8)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-mini-profiler (2.3.4)
rack (>= 1.2.0)
rack-session (2.0.0)
rack (>= 3.0.0)
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.8.5)
actioncable (= 7.0.8.5)
actionmailbox (= 7.0.8.5)
actionmailer (= 7.0.8.5)
actionpack (= 7.0.8.5)
actiontext (= 7.0.8.5)
actionview (= 7.0.8.5)
activejob (= 7.0.8.5)
activemodel (= 7.0.8.5)
activerecord (= 7.0.8.5)
activestorage (= 7.0.8.5)
activesupport (= 7.0.8.5)
rackup (2.2.1)
rack (>= 3)
rails (7.2.2.1)
actioncable (= 7.2.2.1)
actionmailbox (= 7.2.2.1)
actionmailer (= 7.2.2.1)
actionpack (= 7.2.2.1)
actiontext (= 7.2.2.1)
actionview (= 7.2.2.1)
activejob (= 7.2.2.1)
activemodel (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
bundler (>= 1.15.0)
railties (= 7.0.8.5)
railties (= 7.2.2.1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
rails-html-sanitizer (1.6.1)
loofah (~> 2.21)
nokogiri (~> 1.14)
rails_admin (3.1.3)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails_admin (3.3.0)
activemodel-serializers-xml (>= 1.0)
csv
kaminari (>= 0.14, < 2.0)
nested_form (~> 0.3)
rails (>= 6.0, < 8)
turbo-rails (~> 1.0)
railties (7.0.8.5)
actionpack (= 7.0.8.5)
activesupport (= 7.0.8.5)
method_source
rails (>= 6.0, < 9)
turbo-rails (>= 1.0, < 3)
railties (7.2.2.1)
actionpack (= 7.2.2.1)
activesupport (= 7.2.2.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
randexp (0.1.7)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rdoc (6.8.1)
psych (>= 4.0.0)
redcarpet (3.6.0)
redis (4.8.1)
redis-client (0.22.1)
connection_pool
regexp_parser (2.9.0)
request_store (1.6.0)
reline (0.5.12)
io-console (~> 0.5)
request_store (1.7.0)
rack (>= 1.4)
responders (3.1.1)
actionpack (>= 5.2)
@ -434,6 +461,7 @@ GEM
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
securerandom (0.4.0)
selenium-webdriver (4.18.1)
base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5)
@ -462,21 +490,22 @@ GEM
smart_properties (1.17.0)
stimulus-rails (1.3.3)
railties (>= 6.0.0)
stringio (3.1.2)
thor (1.3.2)
thread_safe (0.3.6)
timecop (0.9.8)
timeout (0.4.1)
turbo-rails (1.5.0)
timeout (0.4.2)
turbo-rails (2.0.11)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uk_postcode (2.1.8)
unicode-display_width (2.5.0)
unread (0.13.1)
unread (0.14.0)
activerecord (>= 6.1)
uri (0.13.0)
useragent (0.16.11)
view_component (3.10.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
@ -532,8 +561,8 @@ DEPENDENCIES
excon (~> 0.111.0)
factory_bot_rails
faker
govuk-components (~> 5.1)
govuk_design_system_formbuilder (~> 5.0)
govuk-components (~> 5.7)
govuk_design_system_formbuilder (~> 5.7)
govuk_markdown
jsbundling-rails
json-schema
@ -541,19 +570,19 @@ DEPENDENCIES
method_source (~> 1.1)
notifications-ruby-client
overcommit (>= 0.37.0)
paper_trail
paper_trail (~> 15.2)
paper_trail-globalid
parallel_tests
pg (~> 1.1)
possessive
propshaft
pry-byebug
puma (~> 5.6)
puma (~> 6.4)
pundit
rack (>= 2.2.6.3)
rack-attack
rack-mini-profiler (~> 2.0)
rails (~> 7.0.8.5)
rails (~> 7.2.2)
rails_admin (~> 3.1)
redcarpet (~> 3.6)
redis (~> 4.8)

2
app/components/bulk_upload_error_row_component.html.erb

@ -38,7 +38,7 @@
<% if potential_errors.any? %>
<h2 class="govuk-heading-m">Potential errors</h2>
<p class="govuk-body">The following groups of cells might have conflicting data. Check the answers and fix any incorrect data.<br><br>If the answers are correct, fix the critical errors and reupload the file. You'll need to confirm that the following data is correct when the file only contains potential errors.</p>
<p class="govuk-body">The following groups of cells might have conflicting data. Check the answers and fix any incorrect data.<br><br>If the answers are correct, fix the critical errors and upload the file again. You'll need to confirm that the following data is correct when the file only contains potential errors.</p>
<%= govuk_table(html_attributes: { class: "no-bottom-border" }) do |table| %>
<%= table.with_head do |head| %>
<% head.with_row do |row| %>

22
app/components/primary_navigation_component.html.erb

@ -1,17 +1,5 @@
<nav class="app-primary-navigation" aria-label="primary">
<div class="govuk-width-container">
<ul class="app-primary-navigation__list">
<% items.each do |item| %>
<% if item.current %>
<li class="app-primary-navigation__item app-primary-navigation__item--current">
<%= govuk_link_to item[:text], item[:href], class: "app-primary-navigation__link", aria: { current: "page" } %>
</li>
<% else %>
<li class="app-primary-navigation__item">
<%= govuk_link_to item[:text], item[:href], class: "app-primary-navigation__link" %>
</li>
<% end %>
<% end %>
</ul>
</div>
</nav>
<%= govuk_service_navigation(navigation_id: "primary-navigation", classes: "app-service-navigation") do |sn|
items.each do |item|
sn.with_navigation_item(text: item[:text], href: item[:href], classes: "", current: item[:current])
end
end %>

2
app/components/search_component.html.erb

@ -1,4 +1,4 @@
<%= form_with model: @user, url: path(current_user), method: "get", local: true do |f| %>
<%= form_with url: path(current_user), method: "get", local: true do |f| %>
<div class="app-search govuk-!-margin-bottom-4">
<%= f.govuk_text_field :search,
form_group: {

2
app/components/search_result_caption_component.html.erb

@ -7,7 +7,7 @@
<strong><%= count %></strong> <%= item_label.pluralize(count) %> matching filters<br>
<% else %>
<span class="govuk-!-margin-right-4">
<strong><%= count %></strong> total <%= item %>
<strong><%= count %></strong> total <%= item.pluralize(count) %>
</span>
<% end %>
</span>

23
app/controllers/bulk_upload_lettings_logs_controller.rb

@ -1,6 +1,7 @@
class BulkUploadLettingsLogsController < ApplicationController
before_action :authenticate_user!
before_action :validate_data_protection_agrement_signed!
before_action :validate_data_protection_agreement_signed!
before_action :validate_year!, except: %w[start]
def start
if have_choice_of_year?
@ -24,12 +25,26 @@ class BulkUploadLettingsLogsController < ApplicationController
private
def validate_data_protection_agrement_signed!
def validate_data_protection_agreement_signed!
return if @current_user.organisation.data_protection_confirmed?
redirect_to lettings_logs_path
end
def validate_year!
return if params[:id] == "year"
return if params[:id] == "guidance" && params.dig(:form, :year).nil?
allowed_years = [current_year]
allowed_years.push(current_year - 1) if FormHandler.instance.lettings_in_crossover_period?
allowed_years.push(current_year + 1) if FeatureToggle.allow_future_form_use?
provided_year = params.dig(:form, :year)&.to_i
return if allowed_years.include?(provided_year)
render_not_found
end
def current_year
FormHandler.instance.current_collection_start_year
end
@ -48,8 +63,6 @@ private
Forms::BulkUploadLettings::PrepareYourFile.new(form_params)
when "guidance"
Forms::BulkUploadLettings::Guidance.new(form_params.merge(referrer: params[:referrer]))
when "needstype"
Forms::BulkUploadLettings::Needstype.new(form_params)
when "upload-your-file"
Forms::BulkUploadLettings::UploadYourFile.new(form_params.merge(current_user:))
when "checking-file"
@ -60,6 +73,6 @@ private
end
def form_params
params.fetch(:form, {}).permit(:year, :needstype, :file, :organisation_id)
params.fetch(:form, {}).permit(:year, :file, :organisation_id)
end
end

19
app/controllers/bulk_upload_sales_logs_controller.rb

@ -1,6 +1,7 @@
class BulkUploadSalesLogsController < ApplicationController
before_action :authenticate_user!
before_action :validate_data_protection_agrement_signed!
before_action :validate_data_protection_agreement_signed!
before_action :validate_year!, except: %w[start]
def start
if have_choice_of_year?
@ -24,12 +25,26 @@ class BulkUploadSalesLogsController < ApplicationController
private
def validate_data_protection_agrement_signed!
def validate_data_protection_agreement_signed!
return if @current_user.organisation.data_protection_confirmed?
redirect_to sales_logs_path
end
def validate_year!
return if params[:id] == "year"
return if params[:id] == "guidance" && params.dig(:form, :year).nil?
allowed_years = [current_year]
allowed_years.push(current_year - 1) if FormHandler.instance.sales_in_crossover_period?
allowed_years.push(current_year + 1) if FeatureToggle.allow_future_form_use?
provided_year = params.dig(:form, :year)&.to_i
return if allowed_years.include?(provided_year)
render_not_found
end
def current_year
FormHandler.instance.current_collection_start_year
end

49
app/controllers/form_controller.rb

@ -5,6 +5,7 @@ class FormController < ApplicationController
before_action :find_resource, only: %i[review]
before_action :find_resource_by_named_id, except: %i[review]
before_action :check_collection_period, only: %i[submit_form show_page]
before_action :set_cache_headers, only: [:show_page]
def submit_form
if @log
@ -38,8 +39,15 @@ class FormController < ApplicationController
error_attributes = @log.errors.map(&:attribute)
Rails.logger.info "User triggered validation(s) on: #{error_attributes.join(', ')}"
@subsection = form.subsection_for_page(@page)
restore_error_field_values(@page&.questions)
render "form/page"
flash[:errors] = @log.errors.each_with_object({}) do |error, result|
if @page.questions.map(&:id).include?(error.attribute.to_s)
result[error.attribute.to_s] = error.message
end
end
flash[:log_data] = responses_for_page
question_ids = (@log.errors.map(&:attribute) - [:base]).uniq
flash[:pages_with_errors_count] = question_ids.map { |id| @log.form.get_question(id, @log)&.page&.id }.compact.uniq.count
redirect_to send("#{@log.class.name.underscore}_#{@page.id}_path", @log, { referrer: request.params["referrer"], original_page_id: request.params["original_page_id"], related_question_ids: request.params["related_question_ids"] })
end
else
render_not_found
@ -80,11 +88,17 @@ class FormController < ApplicationController
page_id = request.path.split("/")[-1].underscore
@page = form.get_page(page_id)
@subsection = form.subsection_for_page(@page)
@pages_with_errors_count = 0
if @page.routed_to?(@log, current_user) || is_referrer_type?("interruption_screen") || adding_answer_from_check_errors_page?
if updated_answer_from_check_errors_page?
@questions = request.params["related_question_ids"].map { |id| @log.form.get_question(id, @log) }
render "form/check_errors"
else
if flash[:errors].present?
restore_previous_errors(flash[:errors])
restore_error_field_values(flash[:log_data])
@pages_with_errors_count = flash[:pages_with_errors_count]
end
render "form/page"
end
else
@ -97,14 +111,27 @@ class FormController < ApplicationController
private
def restore_error_field_values(questions)
return unless questions
def restore_error_field_values(previous_responses)
return unless previous_responses
questions.each do |question|
if question&.type == "date" && @log.attributes.key?(question.id)
@log[question.id] = @log.send("#{question.id}_was")
previous_responses_to_reset = previous_responses.reject do |key, value|
if @log.form.get_question(key, @log)&.type == "date" && value.present?
year = value.split("-").first.to_i
year&.zero?
else
false
end
end
@log.assign_attributes(previous_responses_to_reset)
end
def restore_previous_errors(previous_errors)
return unless previous_errors
previous_errors.each do |attribute, message|
@log.errors.add attribute, message.html_safe
end
end
def responses_for_page(page)
@ -420,7 +447,7 @@ private
@log.valid?
@log.reload
error_attributes = @log.errors.map(&:attribute)
@questions = @log.form.questions.select { |q| error_attributes.include?(q.id.to_sym) }
@questions = @log.form.questions.select { |q| error_attributes.include?(q.id.to_sym) && q.page.routed_to?(@log, current_user) }
end
render "form/check_errors"
end
@ -432,4 +459,10 @@ private
def updated_answer_from_check_errors_page?
params["check_errors"]
end
def set_cache_headers
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "Mon, 01 Jan 1990 00:00:00 GMT"
end
end

2
app/controllers/logs_controller.rb

@ -38,7 +38,7 @@ private
API_ACTIONS = %w[create show update].freeze
def json_api_request?
API_ACTIONS.include?(request["action"]) && request.format.json?
API_ACTIONS.include?(params["action"]) && request.format.json?
end
def authenticate

1
app/controllers/organisations_controller.rb

@ -155,6 +155,7 @@ class OrganisationsController < ApplicationController
end
redirect_to details_organisation_path(@organisation)
else
@used_rent_periods = @organisation.lettings_logs.pluck(:period).uniq.compact.map(&:to_s)
@rent_periods = helpers.rent_periods_with_checked_attr(checked_periods: selected_rent_periods)
render :edit, status: :unprocessable_entity
end

4
app/controllers/schemes_controller.rb

@ -150,7 +150,7 @@ class SchemesController < ApplicationController
@scheme.update!(secondary_client_group: nil) if @scheme.has_other_client_group == "No"
if scheme_params[:confirmed] == "true" || @scheme.confirmed?
if check_answers && should_direct_via_secondary_client_group_page?(page)
redirect_to scheme_secondary_client_group_path(@scheme, referrer: "check-answers")
redirect_to scheme_secondary_client_group_path(@scheme, referrer: "has-other-client-group")
else
@scheme.locations.update!(confirmed: true)
flash[:notice] = if scheme_previously_confirmed
@ -162,7 +162,7 @@ class SchemesController < ApplicationController
end
elsif check_answers
if should_direct_via_secondary_client_group_page?(page)
redirect_to scheme_secondary_client_group_path(@scheme, referrer: "check-answers")
redirect_to scheme_secondary_client_group_path(@scheme, referrer: "has-other-client-group")
else
redirect_to scheme_check_answers_path(@scheme)
end

12
app/controllers/sessions_controller.rb

@ -1,20 +1,20 @@
class SessionsController < ApplicationController
def clear_filters
session[session_name_for(params[:filter_type])] = "{}"
path_params = params[:path_params].presence || {}
filter_path_params = params[:filter_path_params].presence || {}
if path_params[:organisation_id].present?
redirect_to send("#{params[:filter_type]}_organisation_path", id: path_params[:organisation_id], scheme_id: path_params[:scheme_id], search: path_params[:search])
if filter_path_params[:organisation_id].present?
redirect_to send("#{params[:filter_type]}_organisation_path", id: filter_path_params[:organisation_id], scheme_id: filter_path_params[:scheme_id], search: filter_path_params[:search])
elsif params[:filter_type].include?("bulk_uploads")
bulk_upload_type = params[:filter_type].split("_").first
uploading_organisation = params[:organisation_id].presence
if uploading_organisation.present?
redirect_to send("bulk_uploads_#{bulk_upload_type}_logs_path", search: path_params[:search], uploading_organisation:)
redirect_to send("bulk_uploads_#{bulk_upload_type}_logs_path", search: filter_path_params[:search], uploading_organisation:)
else
redirect_to send("bulk_uploads_#{bulk_upload_type}_logs_path", search: path_params[:search])
redirect_to send("bulk_uploads_#{bulk_upload_type}_logs_path", search: filter_path_params[:search])
end
else
redirect_to send("#{params[:filter_type]}_path", scheme_id: path_params[:scheme_id], search: path_params[:search])
redirect_to send("#{params[:filter_type]}_path", scheme_id: filter_path_params[:scheme_id], search: filter_path_params[:search])
end
end

69
app/frontend/styles/_primary-navigation.scss

@ -1,69 +0,0 @@
.app-primary-navigation {
@include govuk-font(19, $weight: bold);
background-color: govuk-colour("light-grey");
border-bottom: 1px solid $govuk-border-colour;
}
.govuk-phase-banner + .app-primary-navigation {
margin-top: -1px;
}
.app-primary-navigation__list {
@include govuk-clearfix;
left: govuk-spacing(-3);
list-style: none;
margin: 0;
padding: 0;
position: relative;
right: govuk-spacing(-3);
width: calc(100% + #{govuk-spacing(6)});
}
.app-primary-navigation__item {
box-sizing: border-box;
display: block;
float: left;
line-height: 50px;
height: 50px;
padding: 0 govuk-spacing(3);
position: relative;
}
.app-primary-navigation__item--current {
border-bottom: $govuk-border-width-narrow solid $govuk-link-colour;
&:hover {
border-bottom-color: $govuk-link-hover-colour;
}
&:active {
border-bottom-color: $govuk-link-active-colour;
}
}
.app-primary-navigation__item--align-right {
@include govuk-media-query($from: tablet) {
float: right;
}
}
.app-primary-navigation__link {
@include govuk-link-common;
@include govuk-link-style-no-visited-state;
@include govuk-link-style-no-underline;
@include govuk-typography-weight-bold;
// Extend the touch area of the link to the list
&::after {
bottom: 0;
content: "";
left: 0;
position: absolute;
right: 0;
top: 0;
}
}
.app-primary-navigation__item--current .app-primary-navigation__link:hover {
text-decoration: none;
}

42
app/frontend/styles/application.scss

@ -45,7 +45,6 @@ $govuk-breakpoints: (
@import "task-list";
@import "template";
@import "panel";
@import "primary-navigation";
@import "search";
@import "sub-navigation";
@import "unread-notification";
@ -86,3 +85,44 @@ $govuk-breakpoints: (
.govuk-notification-banner__content > * {
max-width: fit-content;
}
.govuk-service-navigation__active-fallback,
.govuk-service-navigation__list {
font-weight: bold;
}
.govuk-service-navigation__link {
@include govuk-link-common;
@include govuk-link-style-no-visited-state;
@include govuk-link-style-no-underline;
@include govuk-typography-weight-bold;
// Extend the touch area of the link to the list
&::after {
bottom: 0;
content: "";
left: 0;
position: absolute;
right: 0;
top: 0;
}
}
.govuk-service-navigation__item--active {
border-bottom-width: 4px;
}
.govuk-service-navigation__item {
padding-right: 15px;
padding-left: 15px;
margin: 0;
}
.govuk-service-navigation__item:not(:last-child) {
margin-right: 0;
}
.govuk-service-navigation__container {
left: -15px;
position: relative;
}

4
app/helpers/filters_helper.rb

@ -165,9 +165,9 @@ module FiltersHelper
applied_filters_count(filter_type).zero? ? "No filters applied" : "#{pluralize(applied_filters_count(filter_type), 'filter')} applied"
end
def reset_filters_link(filter_type, path_params = {})
def reset_filters_link(filter_type, filter_path_params = {})
if applied_filters_count(filter_type).positive?
govuk_link_to "Clear", clear_filters_path(filter_type:, path_params:)
govuk_link_to "Clear", clear_filters_path(filter_type:, filter_path_params:)
end
end

4
app/helpers/form_page_error_helper.rb

@ -12,8 +12,4 @@ module FormPageErrorHelper
errors.each { |error| lettings_log.errors.delete(error.attribute) }
end
end
def all_questions_affected_by_errors(log)
(log.errors.map(&:attribute) - [:base]).uniq
end
end

2
app/helpers/logs_helper.rb

@ -10,7 +10,7 @@ module LogsHelper
def bulk_upload_options(bulk_upload)
array = bulk_upload ? [bulk_upload.id] : []
array.index_with { |_bulk_upload_id| "With logs from bulk upload" }
array.index_with { |_bulk_upload_id| "Only logs from this bulk upload" }
end
def search_label_for_controller(controller)

18
app/helpers/schemes_helper.rb

@ -101,6 +101,24 @@ module SchemesHelper
organisation.owned_schemes.duplicate_sets.any? || organisation.owned_schemes.any? { |scheme| scheme.locations.duplicate_sets.any? }
end
def scheme_back_button_path(scheme, current_page)
return scheme_check_answers_path(scheme) if request.params[:referrer] == "check-answers"
return scheme_confirm_secondary_client_group_path(scheme, referrer: "check-answers") if request.params[:referrer] == "has-other-client-group"
case current_page
when "details"
schemes_path
when "primary_client_group"
scheme_details_path(scheme)
when "confirm_secondary_client_group"
scheme_primary_client_group_path(scheme)
when "secondary_client_group"
scheme_confirm_secondary_client_group_path(scheme)
when "support"
scheme.has_other_client_group == "Yes" ? scheme_secondary_client_group_path(scheme) : scheme_confirm_secondary_client_group_path(scheme)
end
end
private
ActivePeriod = Struct.new(:from, :to)

21
app/mailers/bulk_upload_mailer.rb

@ -3,6 +3,7 @@ class BulkUploadMailer < NotifyMailer
COMPLETE_TEMPLATE_ID = "83279578-c890-4168-838b-33c9cf0dc9f0".freeze
FAILED_CSV_ERRORS_TEMPLATE_ID = "e27abcd4-5295-48c2-b127-e9ee4b781b75".freeze
FAILED_CSV_DUPLICATE_ERRORS_TEMPLATE_ID = "931d5bda-a08f-4de6-a455-38a63bff1956".freeze
FAILED_FILE_SETUP_ERROR_TEMPLATE_ID = "24c9f4c7-96ad-470a-ba31-eb51b7cbafd9".freeze
FAILED_SERVICE_ERROR_TEMPLATE_ID = "c3f6288c-7a74-4e77-99ee-6c4a0f6e125a".freeze
HOW_TO_FIX_UPLOAD_TEMPLATE_ID = "21a07b26-f625-4846-9f4d-39e30937aa24".freeze
@ -95,6 +96,26 @@ class BulkUploadMailer < NotifyMailer
)
end
def send_correct_duplicates_and_upload_again_mail(bulk_upload:)
summary_report_link = if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors?
bulk_upload.sales? ? summary_bulk_upload_sales_result_url(bulk_upload) : summary_bulk_upload_lettings_result_url(bulk_upload)
else
bulk_upload.sales? ? bulk_upload_sales_result_url(bulk_upload) : bulk_upload_lettings_result_url(bulk_upload)
end
send_email(
bulk_upload.user.email,
FAILED_CSV_DUPLICATE_ERRORS_TEMPLATE_ID,
{
filename: bulk_upload.filename,
upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time),
year_combo: bulk_upload.year_combo,
lettings_or_sales: bulk_upload.log_type,
summary_report_link:,
},
)
end
def send_bulk_upload_failed_file_setup_error_mail(bulk_upload:)
bulk_upload_link = if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors?
bulk_upload.sales? ? summary_bulk_upload_sales_result_url(bulk_upload) : summary_bulk_upload_lettings_result_url(bulk_upload)

6
app/models/bulk_upload.rb

@ -1,7 +1,7 @@
class BulkUpload < ApplicationRecord
enum log_type: { lettings: "lettings", sales: "sales" }
enum rent_type_fix_status: { not_applied: "not_applied", applied: "applied", not_needed: "not_needed" }
enum failure_reason: { blank_template: "blank_template", wrong_template: "wrong_template" }
enum :log_type, { lettings: "lettings", sales: "sales" }
enum :rent_type_fix_status, { not_applied: "not_applied", applied: "applied", not_needed: "not_needed" }
enum :failure_reason, { blank_template: "blank_template", wrong_template: "wrong_template" }
belongs_to :user

2
app/models/csv_download.rb

@ -1,5 +1,5 @@
class CsvDownload < ApplicationRecord
enum download_type: { lettings: "lettings", sales: "sales", schemes: "schemes", locations: "locations", combined: "combined" }
enum :download_type, { lettings: "lettings", sales: "sales", schemes: "schemes", locations: "locations", combined: "combined" }
belongs_to :user
belongs_to :organisation

9
app/models/derived_variables/lettings_log_variables.rb

@ -84,6 +84,15 @@ module DerivedVariables::LettingsLogVariables
if uprn_known&.zero?
self.uprn = nil
if uprn_known_was == 1
self.address_line1 = nil
self.address_line2 = nil
self.town_or_city = nil
self.county = nil
self.postcode_known = nil
self.postcode_full = nil
self.la = nil
end
end
if uprn_known == 1 && uprn_confirmed&.zero?

12
app/models/derived_variables/sales_log_variables.rb

@ -53,6 +53,15 @@ module DerivedVariables::SalesLogVariables
if uprn_known&.zero?
self.uprn = nil
if uprn_known_was == 1
self.address_line1 = nil
self.address_line2 = nil
self.town_or_city = nil
self.county = nil
self.pcodenk = nil
self.postcode_full = nil
self.la = nil
end
end
if uprn_known == 1 && uprn_confirmed&.zero?
@ -80,6 +89,9 @@ module DerivedVariables::SalesLogVariables
self.is_la_inferred = false
end
self.numstair = is_firststair? ? 1 : nil if numstair == 1 && firststair_changed?
self.mrent = 0 if stairowned_100?
set_encoded_derived_values!(DEPENDENCIES)
end

3
app/models/export.rb

@ -1,2 +1,5 @@
class Export < ApplicationRecord
scope :lettings, -> { where(collection: "lettings") }
scope :organisations, -> { where(collection: "organisations") }
scope :users, -> { where(collection: "users") }
end

1
app/models/form/lettings/questions/offered.rb

@ -7,7 +7,6 @@ class Form::Lettings::Questions::Offered < ::Form::Question
@check_answers_card_number = 0
@max = 150
@min = 0
@hint_text = I18n.t("hints.offered")
@step = 1
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end

2
app/models/form/sales/pages/about_staircase.rb

@ -18,7 +18,7 @@ class Form::Sales::Pages::AboutStaircase < ::Form::Page
end
def staircase_sale_question
if form.start_date.year >= 2023
if [2023, 2024].include?(form.start_date.year)
Form::Sales::Questions::StaircaseSale.new(nil, nil, self)
end
end

2
app/models/form/sales/pages/equity.rb

@ -1,7 +1,7 @@
class Form::Sales::Pages::Equity < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "equity"
@copy_key = "sales.sale_information.equity"
end
def questions

17
app/models/form/sales/pages/monthly_rent_staircasing.rb

@ -0,0 +1,17 @@
class Form::Sales::Pages::MonthlyRentStaircasing < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "monthly_rent_staircasing"
@copy_key = "sales.sale_information.mrent_staircasing"
@depends_on = [{
"stairowned_100?" => false,
}]
end
def questions
@questions ||= [
Form::Sales::Questions::MonthlyRentBeforeStaircasing.new(nil, nil, self),
Form::Sales::Questions::MonthlyRentAfterStaircasing.new(nil, nil, self),
]
end
end

17
app/models/form/sales/pages/monthly_rent_staircasing_owned.rb

@ -0,0 +1,17 @@
class Form::Sales::Pages::MonthlyRentStaircasingOwned < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "monthly_rent_staircasing_owned"
@copy_key = "sales.sale_information.mrent_staircasing"
@header = ""
@depends_on = [{
"stairowned_100?" => true,
}]
end
def questions
@questions ||= [
Form::Sales::Questions::MonthlyRentBeforeStaircasing.new(nil, nil, self),
]
end
end

2
app/models/form/sales/pages/owning_organisation.rb

@ -20,11 +20,13 @@ class Form::Sales::Pages::OwningOrganisation < ::Form::Page
if current_user.organisation.holds_own_stock?
return true if current_user.organisation.absorbed_organisations.any?(&:holds_own_stock?)
return true if stock_owners.count >= 1
return false if log.owning_organisation == current_user.organisation
log.update!(owning_organisation: current_user.organisation)
else
return false if stock_owners.count.zero?
return true if stock_owners.count > 1
return false if log.owning_organisation == stock_owners.first
log.update!(owning_organisation: stock_owners.first)
end

15
app/models/form/sales/pages/staircase_first_time.rb

@ -0,0 +1,15 @@
class Form::Sales::Pages::StaircaseFirstTime < ::Form::Page
def initialize(id, hsh, subsection)
super(id, hsh, subsection)
@id = "staircase_first_time"
@depends_on = [{
"staircase" => 1,
}]
end
def questions
@questions ||= [
Form::Sales::Questions::StaircaseFirstTime.new(nil, nil, self),
]
end
end

16
app/models/form/sales/pages/staircase_initial_date.rb

@ -0,0 +1,16 @@
class Form::Sales::Pages::StaircaseInitialDate < ::Form::Page
def initialize(id, hsh, subsection)
super(id, hsh, subsection)
@id = "staircase_initial_date"
@header = ""
@depends_on = [{
"is_firststair?" => true,
}]
end
def questions
@questions ||= [
Form::Sales::Questions::StaircaseInitialDate.new(nil, nil, self),
]
end
end

18
app/models/form/sales/pages/staircase_previous.rb

@ -0,0 +1,18 @@
class Form::Sales::Pages::StaircasePrevious < ::Form::Page
def initialize(id, hsh, subsection)
super(id, hsh, subsection)
@id = "staircase_previous"
@copy_key = "sales.sale_information.stairprevious"
@depends_on = [{
"is_firststair?" => false,
}]
end
def questions
@questions ||= [
Form::Sales::Questions::StaircaseCount.new(nil, nil, self),
Form::Sales::Questions::StaircaseLastDate.new(nil, nil, self),
Form::Sales::Questions::StaircaseInitialDate.new(nil, nil, self),
]
end
end

17
app/models/form/sales/pages/staircase_sale.rb

@ -0,0 +1,17 @@
class Form::Sales::Pages::StaircaseSale < ::Form::Page
def initialize(id, hsh, subsection)
super(id, hsh, subsection)
@id = "staircase_sale"
@copy_key = form.start_year_2025_or_later? ? "sales.sale_information.staircasesale" : "sales.sale_information.about_staircasing.staircasesale"
@depends_on = [{
"staircase" => 1,
"stairowned" => 100,
}]
end
def questions
@questions ||= [
Form::Sales::Questions::StaircaseSale.new(nil, nil, self),
]
end
end

2
app/models/form/sales/pages/value_shared_ownership.rb

@ -1,7 +1,7 @@
class Form::Sales::Pages::ValueSharedOwnership < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "value_shared_ownership"
@copy_key = "sales.sale_information.value"
end
def questions

2
app/models/form/sales/questions/buyer2_income_known.rb

@ -2,7 +2,7 @@ class Form::Sales::Questions::Buyer2IncomeKnown < ::Form::Question
def initialize(id, hsh, page)
super
@id = "income2nk"
@copy_key = "sales.income_benefits_and_savings.buyer_2_income.income2"
@copy_key = "sales.income_benefits_and_savings.buyer_2_income.income2nk"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@conditional_for = {

6
app/models/form/sales/questions/buyer2_working_situation.rb

@ -15,6 +15,10 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
def displayed_answer_options(_log, _user = nil)
answer_options.reject { |key, _| key == "9" }
end
def answer_options
if form.start_year_2025_or_later?
{
@ -26,6 +30,7 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question
"6" => { "value" => "Not seeking work" },
"7" => { "value" => "Full-time student" },
"8" => { "value" => "Unable to work due to long term sick or disability" },
"9" => { "value" => "Child under 16" },
"0" => { "value" => "Other" },
"10" => { "value" => "Buyer prefers not to say" },
}.freeze
@ -41,6 +46,7 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question
"0" => { "value" => "Other" },
"10" => { "value" => "Buyer prefers not to say" },
"7" => { "value" => "Full-time student" },
"9" => { "value" => "Child under 16" },
}.freeze
end
end

1
app/models/form/sales/questions/equity.rb

@ -2,6 +2,7 @@ class Form::Sales::Questions::Equity < ::Form::Question
def initialize(id, hsh, page)
super
@id = "equity"
@copy_key = form.start_year_2025_or_later? ? "sales.sale_information.equity.#{page.id}" : "sales.sale_information.equity"
@type = "numeric"
@min = 0
@max = 100

15
app/models/form/sales/questions/monthly_rent_after_staircasing.rb

@ -0,0 +1,15 @@
class Form::Sales::Questions::MonthlyRentAfterStaircasing < ::Form::Question
def initialize(id, hsh, page)
super
@id = "mrent"
@copy_key = "sales.sale_information.mrent_staircasing.poststaircasing"
@type = "numeric"
@min = 0
@step = 0.01
@width = 5
@prefix = "£"
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
QUESTION_NUMBER_FROM_YEAR = { 2025 => 99 }.freeze
end

15
app/models/form/sales/questions/monthly_rent_before_staircasing.rb

@ -0,0 +1,15 @@
class Form::Sales::Questions::MonthlyRentBeforeStaircasing < ::Form::Question
def initialize(id, hsh, page)
super
@id = "mrentprestaircasing"
@copy_key = "sales.sale_information.mrent_staircasing.prestaircasing"
@type = "numeric"
@min = 0
@step = 0.01
@width = 5
@prefix = "£"
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
QUESTION_NUMBER_FROM_YEAR = { 2025 => 98 }.freeze
end

2
app/models/form/sales/questions/mortgageused.rb

@ -12,7 +12,7 @@ class Form::Sales::Questions::Mortgageused < ::Form::Question
def displayed_answer_options(log, _user = nil)
if log.outright_sale? && log.saledate && !form.start_year_2024_or_later?
answer_options_without_dont_know
elsif log.stairowned == 100 || log.outright_sale?
elsif log.stairowned_100? || log.outright_sale? || (log.is_staircase? && form.start_year_2025_or_later?)
ANSWER_OPTIONS
else
answer_options_without_dont_know

15
app/models/form/sales/questions/staircase_count.rb

@ -0,0 +1,15 @@
class Form::Sales::Questions::StaircaseCount < ::Form::Question
def initialize(id, hsh, page)
super
@id = "numstair"
@copy_key = "sales.sale_information.stairprevious.numstair"
@type = "numeric"
@width = 2
@min = 2
@max = 10
@step = 1
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
QUESTION_NUMBER_FROM_YEAR = { 2025 => 82 }.freeze
end

16
app/models/form/sales/questions/staircase_first_time.rb

@ -0,0 +1,16 @@
class Form::Sales::Questions::StaircaseFirstTime < ::Form::Question
def initialize(id, hsh, page)
super
@id = "firststair"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
ANSWER_OPTIONS = {
"1" => { "value" => "Yes" },
"2" => { "value" => "No" },
}.freeze
QUESTION_NUMBER_FROM_YEAR = { 2025 => 81 }.freeze
end

11
app/models/form/sales/questions/staircase_initial_date.rb

@ -0,0 +1,11 @@
class Form::Sales::Questions::StaircaseInitialDate < ::Form::Question
def initialize(id, hsh, page)
super
@id = "initialpurchase"
@copy_key = "sales.sale_information.stairprevious.initialpurchase"
@type = "date"
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
QUESTION_NUMBER_FROM_YEAR = { 2025 => 83 }.freeze
end

11
app/models/form/sales/questions/staircase_last_date.rb

@ -0,0 +1,11 @@
class Form::Sales::Questions::StaircaseLastDate < ::Form::Question
def initialize(id, hsh, page)
super
@id = "lasttransaction"
@copy_key = "sales.sale_information.stairprevious.lasttransaction"
@type = "date"
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
QUESTION_NUMBER_FROM_YEAR = { 2025 => 83 }.freeze
end

2
app/models/form/sales/questions/staircase_sale.rb

@ -2,7 +2,7 @@ class Form::Sales::Questions::StaircaseSale < ::Form::Question
def initialize(id, hsh, page)
super
@id = "staircasesale"
@copy_key = "sales.sale_information.about_staircasing.staircasesale"
@copy_key = form.start_year_2025_or_later? ? "sales.sale_information.staircasesale" : "sales.sale_information.about_staircasing.staircasesale"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]

1
app/models/form/sales/questions/value.rb

@ -2,6 +2,7 @@ class Form::Sales::Questions::Value < ::Form::Question
def initialize(id, hsh, page)
super
@id = "value"
@copy_key = form.start_year_2025_or_later? ? "sales.sale_information.value.#{page.id}" : "sales.sale_information.value"
@type = "numeric"
@min = 0
@step = 1

16
app/models/form/sales/sections/sale_information.rb

@ -4,18 +4,20 @@ class Form::Sales::Sections::SaleInformation < ::Form::Section
@id = "sale_information"
@label = "Sale information"
@description = ""
@subsections = [
shared_ownership_scheme_subsection,
Form::Sales::Subsections::DiscountedOwnershipScheme.new(nil, nil, self),
Form::Sales::Subsections::OutrightSale.new(nil, nil, self),
] || []
@subsections = []
@subsections.concat(shared_ownership_scheme_subsection)
@subsections << Form::Sales::Subsections::DiscountedOwnershipScheme.new(nil, nil, self)
@subsections << Form::Sales::Subsections::OutrightSale.new(nil, nil, self)
end
def shared_ownership_scheme_subsection
if form.start_year_2025_or_later?
Form::Sales::Subsections::SharedOwnershipInitialPurchase.new(nil, nil, self)
[
Form::Sales::Subsections::SharedOwnershipInitialPurchase.new(nil, nil, self),
Form::Sales::Subsections::SharedOwnershipStaircasingTransaction.new(nil, nil, self),
]
else
Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self)
[Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self)]
end
end
end

5
app/models/form/sales/subsections/shared_ownership_initial_purchase.rb

@ -4,6 +4,7 @@ class Form::Sales::Subsections::SharedOwnershipInitialPurchase < ::Form::Subsect
@id = "shared_ownership_initial_purchase"
@label = "Shared ownership - initial purchase"
@depends_on = [{ "ownershipsch" => 1, "setup_completed?" => true, "staircase" => 2 }]
@copy_key = "sale_information"
end
def pages
@ -18,9 +19,9 @@ class Form::Sales::Subsections::SharedOwnershipInitialPurchase < ::Form::Subsect
Form::Sales::Pages::PreviousBedrooms.new(nil, nil, self),
Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self),
Form::Sales::Pages::PreviousTenure.new(nil, nil, self),
Form::Sales::Pages::ValueSharedOwnership.new(nil, nil, self),
Form::Sales::Pages::ValueSharedOwnership.new("value_shared_ownership", nil, self),
Form::Sales::Pages::AboutPriceValueCheck.new("about_price_shared_ownership_value_check", nil, self),
Form::Sales::Pages::Equity.new(nil, nil, self),
Form::Sales::Pages::Equity.new("initial_equity", nil, self),
Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_equity_value_check", nil, self),
Form::Sales::Pages::Mortgageused.new("mortgage_used_shared_ownership", nil, self, ownershipsch: 1),
Form::Sales::Pages::MortgageValueCheck.new("mortgage_used_mortgage_value_check", nil, self),

4
app/models/form/sales/subsections/shared_ownership_scheme.rb

@ -27,9 +27,9 @@ class Form::Sales::Subsections::SharedOwnershipScheme < ::Form::Subsection
Form::Sales::Pages::PreviousBedrooms.new(nil, nil, self),
Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self),
Form::Sales::Pages::PreviousTenure.new(nil, nil, self),
Form::Sales::Pages::ValueSharedOwnership.new(nil, nil, self),
Form::Sales::Pages::ValueSharedOwnership.new("value_shared_ownership", nil, self),
Form::Sales::Pages::AboutPriceValueCheck.new("about_price_shared_ownership_value_check", nil, self),
Form::Sales::Pages::Equity.new(nil, nil, self),
Form::Sales::Pages::Equity.new("equity", nil, self),
Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_equity_value_check", nil, self),
Form::Sales::Pages::Mortgageused.new("mortgage_used_shared_ownership", nil, self, ownershipsch: 1),
Form::Sales::Pages::MortgageValueCheck.new("mortgage_used_mortgage_value_check", nil, self),

35
app/models/form/sales/subsections/shared_ownership_staircasing_transaction.rb

@ -0,0 +1,35 @@
class Form::Sales::Subsections::SharedOwnershipStaircasingTransaction < ::Form::Subsection
def initialize(id, hsh, section)
super
@id = "shared_ownership_staircasing_transaction"
@label = "Shared ownership - staircasing transaction"
@depends_on = [{ "ownershipsch" => 1, "setup_completed?" => true, "staircase" => 1 }]
@copy_key = "sale_information"
end
def pages
@pages ||= [
Form::Sales::Pages::AboutStaircase.new("about_staircasing_joint_purchase", nil, self, joint_purchase: true),
Form::Sales::Pages::AboutStaircase.new("about_staircasing_not_joint_purchase", nil, self, joint_purchase: false),
Form::Sales::Pages::StaircaseSale.new(nil, nil, self),
Form::Sales::Pages::StaircaseBoughtValueCheck.new(nil, nil, self),
Form::Sales::Pages::StaircaseOwnedValueCheck.new("staircase_owned_value_check_joint_purchase", nil, self, joint_purchase: true),
Form::Sales::Pages::StaircaseOwnedValueCheck.new("staircase_owned_value_check_not_joint_purchase", nil, self, joint_purchase: false),
Form::Sales::Pages::StaircaseFirstTime.new(nil, nil, self),
Form::Sales::Pages::StaircasePrevious.new(nil, nil, self),
Form::Sales::Pages::StaircaseInitialDate.new(nil, nil, self),
Form::Sales::Pages::ValueSharedOwnership.new("value_shared_ownership_staircase", nil, self),
Form::Sales::Pages::AboutPriceValueCheck.new("about_price_shared_ownership_value_check", nil, self),
Form::Sales::Pages::Equity.new("staircase_equity", nil, self),
Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_equity_value_check", nil, self),
Form::Sales::Pages::Mortgageused.new("staircase_mortgage_used_shared_ownership", nil, self, ownershipsch: 1),
Form::Sales::Pages::MonthlyRentStaircasingOwned.new(nil, nil, self),
Form::Sales::Pages::MonthlyRentStaircasing.new(nil, nil, self),
Form::Sales::Pages::MonthlyChargesValueCheck.new("monthly_charges_shared_ownership_value_check", nil, self),
].compact
end
def displayed_in_tasklist?(log)
log.staircase == 1 && (log.ownershipsch.nil? || log.ownershipsch == 1)
end
end

39
app/models/forms/bulk_upload_lettings/needstype.rb

@ -1,39 +0,0 @@
module Forms
module BulkUploadLettings
class Needstype
include ActiveModel::Model
include ActiveModel::Attributes
include Rails.application.routes.url_helpers
attribute :needstype, :integer
attribute :year, :integer
attribute :organisation_id, :integer
validates :needstype, presence: true
def view_path
"bulk_upload_lettings_logs/forms/needstype"
end
def options
[OpenStruct.new(id: 1, name: "General needs"), OpenStruct.new(id: 2, name: "Supported housing")]
end
def back_path
bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year:, needstype:, organisation_id: }.compact)
end
def next_path
bulk_upload_lettings_log_path(id: "upload-your-file", form: { year:, needstype:, organisation_id: }.compact)
end
def year_combo
"#{year} to #{year + 1}"
end
def save!
true
end
end
end
end

2
app/models/forms/bulk_upload_lettings_resume/fix_choice.rb

@ -14,7 +14,7 @@ module Forms
def options
[
OpenStruct.new(id: "create-fix-inline", name: "Upload these logs and fix errors on CORE site"),
OpenStruct.new(id: "upload-again", name: "Fix errors in the CSV and re-upload"),
OpenStruct.new(id: "upload-again", name: "Fix errors in the CSV and upload the file again"),
]
end

2
app/models/forms/bulk_upload_sales_resume/fix_choice.rb

@ -14,7 +14,7 @@ module Forms
def options
[
OpenStruct.new(id: "create-fix-inline", name: "Upload these logs and fix errors on CORE site"),
OpenStruct.new(id: "upload-again", name: "Fix errors in the CSV and re-upload"),
OpenStruct.new(id: "upload-again", name: "Fix errors in the CSV and upload the file again"),
]
end

10
app/models/location.rb

@ -2,7 +2,8 @@ class Location < ApplicationRecord
validates :postcode, on: :postcode, presence: { message: I18n.t("validations.location.postcode_blank") }
validate :validate_postcode, on: :postcode, if: proc { |model| model.postcode.presence }
validates :location_admin_district, on: :location_admin_district, presence: { message: I18n.t("validations.location_admin_district") }
validates :units, on: :units, presence: { message: I18n.t("validations.location.units") }
validates :units, on: :units, presence: { message: I18n.t("validations.location.units.must_be_number") }
validates :units, on: :units, numericality: { greater_than_or_equal_to: 1, message: I18n.t("validations.location.units.must_be_one_or_more") }
validates :type_of_unit, on: :type_of_unit, presence: { message: I18n.t("validations.location.type_of_unit") }
validates :mobility_type, on: :mobility_type, presence: { message: I18n.t("validations.location.mobility_standards") }
validates :startdate, on: :startdate, presence: { message: I18n.t("validations.location.startdate_invalid") }
@ -170,7 +171,8 @@ class Location < ApplicationRecord
DUPLICATE_LOCATION_ATTRIBUTES = %w[scheme_id postcode mobility_type].freeze
LOCAL_AUTHORITIES = LocalAuthority.all.map { |la| [la.name, la.code] }.to_h
enum local_authorities: LOCAL_AUTHORITIES
attribute :local_authorities, :string
enum :local_authorities, LOCAL_AUTHORITIES
def self.local_authorities_for_current_year
LocalAuthority.all.active(Time.zone.today).england.map { |la| [la.code, la.name] }.to_h
end
@ -183,7 +185,7 @@ class Location < ApplicationRecord
"Missing": "X",
}.freeze
enum mobility_type: MOBILITY_TYPE
enum :mobility_type, MOBILITY_TYPE
TYPE_OF_UNIT = {
"Bungalow": 6,
@ -194,7 +196,7 @@ class Location < ApplicationRecord
"Shared house or hostel": 4,
}.freeze
enum type_of_unit: TYPE_OF_UNIT
enum :type_of_unit, TYPE_OF_UNIT
def self.find_by_id_on_multiple_fields(id)
return if id.nil?

22
app/models/log.rb

@ -18,14 +18,14 @@ class Log < ApplicationRecord
"pending" => 3,
"deleted" => 4,
}.freeze
enum status: STATUS
enum status_cache: STATUS, _prefix: true
enum :status, STATUS
enum :status_cache, STATUS, prefix: true
CREATION_METHOD = {
"single log" => 1,
"bulk upload" => 2,
}.freeze
enum creation_method: CREATION_METHOD, _prefix: true
enum :creation_method, CREATION_METHOD, prefix: true
scope :visible, -> { where(status: %w[not_started in_progress completed]) }
scope :exportable, -> { where(status: %w[not_started in_progress completed deleted]) }
@ -126,9 +126,11 @@ class Log < ApplicationRecord
end
def address_options
return @address_options if @address_options
return @address_options if @address_options && @last_searched_address_string == address_string
if [address_line1_input, postcode_full_input].all?(&:present?)
@last_searched_address_string = address_string
service = AddressClient.new(address_string)
service.call
if service.result.blank? || service.error.present?
@ -315,7 +317,11 @@ private
def update_status!
return if skip_update_status
self.status = calculate_status
if status == "pending"
self.status_cache = calculate_status
else
self.status = calculate_status
end
end
def all_subsections_completed?
@ -371,14 +377,14 @@ private
end
def reset_location_fields!
reset_location(is_la_inferred, "la", "is_la_inferred", "postcode_full", 1)
reset_log_location(is_la_inferred, "la", "is_la_inferred", "postcode_full", 1)
end
def reset_previous_location_fields!
reset_location(is_previous_la_inferred, "prevloc", "is_previous_la_inferred", "ppostcode_full", previous_la_known)
reset_log_location(is_previous_la_inferred, "prevloc", "is_previous_la_inferred", "ppostcode_full", previous_la_known)
end
def reset_location(is_inferred, la_key, is_inferred_key, postcode_key, is_la_known)
def reset_log_location(is_inferred, la_key, is_inferred_key, postcode_key, is_la_known)
if is_inferred || is_la_known != 1
self[la_key] = nil
end

4
app/models/merge_request.rb

@ -13,7 +13,9 @@ class MergeRequest < ApplicationRecord
request_merged: "request_merged",
deleted: "deleted",
}.freeze
enum status: STATUS
attribute :status, :string
enum :status, STATUS
scope :not_merged, -> { where(request_merged: [false, nil]) }
scope :merged, -> { where(request_merged: true) }

3
app/models/organisation.rb

@ -53,11 +53,12 @@ class Organisation < ApplicationRecord
PRP: 2,
}.freeze
enum provider_type: PROVIDER_TYPE
enum :provider_type, PROVIDER_TYPE
alias_method :la?, :LA?
validates :name, presence: { message: I18n.t("validations.organisation.name_missing") }
validates :name, uniqueness: { case_sensitive: false, message: I18n.t("validations.organisation.name_not_unique") }
validates :provider_type, presence: { message: I18n.t("validations.organisation.provider_type_missing") }
def self.find_by_id_on_multiple_fields(id)

4
app/models/sales_log.rb

@ -557,4 +557,8 @@ class SalesLog < Log
def is_resale?
resale == 1
end
def is_firststair?
firststair == 1
end
end

20
app/models/scheme.rb

@ -145,7 +145,7 @@ class Scheme < ApplicationRecord
Yes: 1,
}.freeze
enum sensitive: SENSITIVE, _suffix: true
enum :sensitive, SENSITIVE, suffix: true
REGISTERED_UNDER_CARE_ACT = {
"Yes – registered care home providing nursing care": 4,
@ -154,7 +154,7 @@ class Scheme < ApplicationRecord
"No": 1,
}.freeze
enum registered_under_care_act: REGISTERED_UNDER_CARE_ACT
enum :registered_under_care_act, REGISTERED_UNDER_CARE_ACT
SCHEME_TYPE = {
"Direct Access Hostel": 5,
@ -164,7 +164,7 @@ class Scheme < ApplicationRecord
"Missing": 0,
}.freeze
enum scheme_type: SCHEME_TYPE, _suffix: true
enum :scheme_type, SCHEME_TYPE, suffix: true
SUPPORT_TYPE = {
"Missing": 0,
@ -175,7 +175,7 @@ class Scheme < ApplicationRecord
"Floating support": 6,
}.freeze
enum support_type: SUPPORT_TYPE, _suffix: true
enum :support_type, SUPPORT_TYPE, suffix: true
PRIMARY_CLIENT_GROUP = {
"Homeless families with support needs": "O",
@ -197,8 +197,8 @@ class Scheme < ApplicationRecord
"Missing": "X",
}.freeze
enum primary_client_group: PRIMARY_CLIENT_GROUP, _suffix: true
enum secondary_client_group: PRIMARY_CLIENT_GROUP, _suffix: true
enum :primary_client_group, PRIMARY_CLIENT_GROUP, suffix: true
enum :secondary_client_group, PRIMARY_CLIENT_GROUP, suffix: true
INTENDED_STAY = {
"Very short stay": "V",
@ -213,8 +213,8 @@ class Scheme < ApplicationRecord
Yes: 1,
}.freeze
enum intended_stay: INTENDED_STAY, _suffix: true
enum has_other_client_group: HAS_OTHER_CLIENT_GROUP, _suffix: true
enum :intended_stay, INTENDED_STAY, suffix: true
enum :has_other_client_group, HAS_OTHER_CLIENT_GROUP, suffix: true
ARRANGEMENT_TYPE = {
"The same organisation that owns the housing stock": "D",
@ -226,7 +226,7 @@ class Scheme < ApplicationRecord
DUPLICATE_SCHEME_ATTRIBUTES = %w[scheme_type registered_under_care_act primary_client_group secondary_client_group has_other_client_group support_type intended_stay].freeze
enum arrangement_type: ARRANGEMENT_TYPE, _suffix: true
enum :arrangement_type, ARRANGEMENT_TYPE, suffix: true
def self.find_by_id_on_multiple_fields(scheme_id, location_id)
return if scheme_id.nil?
@ -329,9 +329,9 @@ class Scheme < ApplicationRecord
def status_at(date)
return :deleted if discarded_at.present?
return :incomplete unless confirmed && locations.confirmed.any?
return :deactivated if owning_organisation.status_at(date) == :deactivated || owning_organisation.status_at(date) == :merged ||
(open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date)
return :incomplete unless confirmed && locations.confirmed.any?
return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date
return :reactivating_soon if last_deactivation_before(date)&.reactivation_date.present? && date < last_deactivation_before(date).reactivation_date
return :activating_soon if startdate.present? && date < startdate

2
app/models/user.rb

@ -56,7 +56,7 @@ class User < ApplicationRecord
unassign: "No, unassign the logs",
}.freeze
enum role: ROLES
enum :role, ROLES
scope :search_by_name, ->(name) { where("users.name ILIKE ?", "%#{name}%") }
scope :search_by_email, ->(email) { where("email ILIKE ?", "%#{email}%") }

2
app/models/validations/property_validations.rb

@ -41,6 +41,8 @@ module Validations::PropertyValidations
def validate_property_postcode(record)
postcode = record.postcode_full
return unless postcode
if record.postcode_known? && (postcode.blank? || !postcode.match(POSTCODE_REGEXP))
error_message = I18n.t("validations.lettings.property.postcode_full.invalid")
record.errors.add :postcode_full, :wrong_format, message: error_message

2
app/models/validations/sales/property_validations.rb

@ -31,6 +31,8 @@ module Validations::Sales::PropertyValidations
def validate_property_postcode(record)
postcode = record.postcode_full
return unless postcode
if record.postcode_known? && (postcode.blank? || !postcode.match(POSTCODE_REGEXP))
error_message = I18n.t("validations.sales.property_information.postcode_full.invalid")
record.errors.add :postcode_full, :wrong_format, message: error_message

25
app/models/validations/sales/sale_information_validations.rb

@ -35,6 +35,14 @@ module Validations::Sales::SaleInformationValidations
end
end
def validate_staircasing_initial_purchase_date(record)
return unless record.initialpurchase
if record.initialpurchase < Time.zone.local(1980, 1, 1)
record.errors.add :initialpurchase, I18n.t("validations.sales.sale_information.initialpurchase.must_be_after_1980")
end
end
def validate_previous_property_unit_type(record)
return unless record.fromprop && record.frombeds
@ -56,7 +64,7 @@ module Validations::Sales::SaleInformationValidations
if over_tolerance?(record.mortgage_deposit_and_grant_total, record.value_with_discount, tolerance, strict: !record.discount.nil?) && record.discounted_ownership_sale?
deposit_and_grant_sentence = record.grant.present? ? ", cash deposit (#{record.field_formatted_as_currency('deposit')}), and grant (#{record.field_formatted_as_currency('grant')})" : " and cash deposit (#{record.field_formatted_as_currency('deposit')})"
discount_sentence = record.discount.present? ? " (#{record.field_formatted_as_currency('value')}) subtracted by the sum of the full purchase price (#{record.field_formatted_as_currency('value')}) multiplied by the percentage discount (#{record.discount}%)" : ""
%i[mortgageused mortgage value deposit ownershipsch discount grant].each do |field|
%i[mortgageused mortgage value deposit discount grant].each do |field|
record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.discounted_ownership_value",
mortgage: record.mortgage&.positive? ? " (#{record.field_formatted_as_currency('mortgage')})" : "",
deposit_and_grant_sentence:,
@ -64,6 +72,12 @@ module Validations::Sales::SaleInformationValidations
discount_sentence:,
value_with_discount: record.field_formatted_as_currency("value_with_discount")).html_safe
end
record.errors.add :ownershipsch, :skip_bu_error, message: I18n.t("validations.sales.sale_information.ownershipsch.discounted_ownership_value",
mortgage: record.mortgage&.positive? ? " (#{record.field_formatted_as_currency('mortgage')})" : "",
deposit_and_grant_sentence:,
mortgage_deposit_and_grant_total: record.field_formatted_as_currency("mortgage_deposit_and_grant_total"),
discount_sentence:,
value_with_discount: record.field_formatted_as_currency("value_with_discount")).html_safe
end
end
@ -351,6 +365,15 @@ module Validations::Sales::SaleInformationValidations
end
end
def validate_number_of_staircase_transactions(record)
return unless record.numstair
if record.firststair == 2 && record.numstair < 2
record.errors.add :numstair, I18n.t("validations.sales.sale_information.numstair.must_be_greater_than_one")
record.errors.add :firststair, I18n.t("validations.sales.sale_information.firststair.cannot_be_no")
end
end
def over_tolerance?(expected, actual, tolerance, strict: false)
if strict
(expected - actual).abs > tolerance

2
app/services/bulk_upload/lettings/log_creator.rb

@ -16,9 +16,7 @@ class BulkUpload::Lettings::LogCreator
row_parser.log.blank_invalid_non_setup_fields!
row_parser.log.bulk_upload = bulk_upload
row_parser.log.creation_method = "bulk upload"
row_parser.log.skip_update_status = true
row_parser.log.status = "pending"
row_parser.log.status_cache = row_parser.log.calculate_status
begin
row_parser.log.save!

14
app/services/bulk_upload/lettings/validator.rb

@ -40,29 +40,25 @@ class BulkUpload::Lettings::Validator
end
end
def create_logs?
return false if any_setup_errors?
def block_log_creation_reason
return "setup_errors" if any_setup_errors?
if row_parsers.any?(&:block_log_creation?)
Sentry.capture_message("Bulk upload log creation blocked: #{bulk_upload.id}.")
return false
return "row_parser_block_log_creation"
end
if any_logs_already_exist? && FeatureToggle.bulk_upload_duplicate_log_check_enabled?
Sentry.capture_message("Bulk upload log creation blocked due to duplicate logs: #{bulk_upload.id}.")
return false
return "duplicate_logs"
end
row_parsers.each do |row_parser|
row_parser.log.blank_invalid_non_setup_fields!
end
if any_logs_invalid?
Sentry.capture_message("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.")
return false
"logs_invalid"
end
true
end
def self.question_for_field(field)

20
app/services/bulk_upload/lettings/year2024/row_parser.rb

@ -607,14 +607,22 @@ private
def validate_uprn_exists_if_any_key_address_fields_are_blank
if field_16.blank? && !key_address_fields_provided?
errors.add(:field_16, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "UPRN."))
%i[field_17 field_19 field_21 field_22].each do |field|
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.address.not_answered")) if send(field).blank?
end
errors.add(:field_16, I18n.t("#{ERROR_BASE_KEY}.address.not_answered", question: "UPRN."))
end
end
def validate_address_option_found
if log.uprn.nil? && field_16.blank? && key_address_fields_provided?
error_message = if log.address_options_present?
I18n.t("#{ERROR_BASE_KEY}.address.not_determined")
else
I18n.t("#{ERROR_BASE_KEY}.address.not_found")
end
%i[field_17 field_18 field_19 field_20 field_21 field_22].each do |field|
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.address.not_found"))
errors.add(field, error_message) if errors[field].blank?
end
end
end
@ -625,19 +633,19 @@ private
def validate_address_fields
if field_16.blank? || log.errors.attribute_names.include?(:uprn)
if field_17.blank?
if field_17.blank? && errors[:field_17].blank?
errors.add(:field_17, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "address line 1."))
end
if field_19.blank?
if field_19.blank? && errors[:field_19].blank?
errors.add(:field_19, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "town or city."))
end
if field_21.blank?
if field_21.blank? && errors[:field_21].blank?
errors.add(:field_21, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 1 of postcode."))
end
if field_22.blank?
if field_22.blank? && errors[:field_22].blank?
errors.add(:field_22, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 2 of postcode."))
end
end

40
app/services/bulk_upload/processor.rb

@ -36,20 +36,28 @@ class BulkUpload::Processor
if validator.any_setup_errors?
send_setup_errors_mail
elsif validator.create_logs?
create_logs
if validator.soft_validation_errors_only?
send_check_soft_validations_mail
elsif created_logs_but_incompleted?
send_how_to_fix_upload_mail
elsif created_logs_and_all_completed?
bulk_upload.unpend
send_success_mail
end
else
send_correct_and_upload_again_mail # summary/full report
block_creation_reason = validator.block_log_creation_reason
if block_creation_reason.present?
case block_creation_reason
when "duplicate_logs"
send_correct_duplicates_and_upload_again_mail
else
send_correct_and_upload_again_mail # summary/full report
end
else
create_logs
if validator.soft_validation_errors_only?
send_check_soft_validations_mail
elsif created_logs_but_incompleted?
send_how_to_fix_upload_mail
elsif created_logs_and_all_completed?
bulk_upload.unpend
send_success_mail
end
end
end
rescue StandardError => e
Sentry.capture_exception(e)
@ -97,6 +105,12 @@ private
.deliver_later
end
def send_correct_duplicates_and_upload_again_mail
BulkUploadMailer
.send_correct_duplicates_and_upload_again_mail(bulk_upload:)
.deliver_later
end
def send_success_mail
BulkUploadMailer
.send_bulk_upload_complete_mail(user:, bulk_upload:)

2
app/services/bulk_upload/sales/log_creator.rb

@ -15,9 +15,7 @@ class BulkUpload::Sales::LogCreator
row_parser.log.blank_invalid_non_setup_fields!
row_parser.log.bulk_upload = bulk_upload
row_parser.log.creation_method = "bulk upload"
row_parser.log.skip_update_status = true
row_parser.log.status = "pending"
row_parser.log.status_cache = row_parser.log.calculate_status
begin
row_parser.log.save!

12
app/services/bulk_upload/sales/validator.rb

@ -39,17 +39,17 @@ class BulkUpload::Sales::Validator
end
end
def create_logs?
return false if any_setup_errors?
def block_log_creation_reason
return "setup_errors" if any_setup_errors?
if row_parsers.any?(&:block_log_creation?)
Sentry.capture_message("Bulk upload log creation blocked: #{bulk_upload.id}.")
return false
return "row_parser_block_log_creation"
end
if any_logs_already_exist? && FeatureToggle.bulk_upload_duplicate_log_check_enabled?
Sentry.capture_message("Bulk upload log creation blocked due to duplicate logs: #{bulk_upload.id}.")
return false
return "duplicate_logs"
end
row_parsers.each do |row_parser|
@ -58,10 +58,8 @@ class BulkUpload::Sales::Validator
if any_logs_invalid?
Sentry.capture_message("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.")
return false
"logs_invalid"
end
true
end
def any_setup_errors?

20
app/services/bulk_upload/sales/year2024/row_parser.rb

@ -605,14 +605,22 @@ private
def validate_uprn_exists_if_any_key_address_fields_are_blank
if field_22.blank? && !key_address_fields_provided?
errors.add(:field_22, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "UPRN."))
%i[field_23 field_25 field_27 field_28].each do |field|
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.address.not_answered")) if send(field).blank?
end
errors.add(:field_22, I18n.t("#{ERROR_BASE_KEY}.address.not_answered", question: "UPRN."))
end
end
def validate_address_option_found
if log.uprn.nil? && field_22.blank? && key_address_fields_provided?
error_message = if log.address_options_present?
I18n.t("#{ERROR_BASE_KEY}.address.not_determined")
else
I18n.t("#{ERROR_BASE_KEY}.address.not_found")
end
%i[field_23 field_24 field_25 field_26 field_27 field_28].each do |field|
errors.add(field, I18n.t("#{ERROR_BASE_KEY}.address.not_found"))
errors.add(field, error_message) if errors[field].blank?
end
end
end
@ -623,19 +631,19 @@ private
def validate_address_fields
if field_22.blank? || log.errors.attribute_names.include?(:uprn)
if field_23.blank?
if field_23.blank? && errors[:field_23].blank?
errors.add(:field_23, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "address line 1."))
end
if field_25.blank?
if field_25.blank? && errors[:field_25].blank?
errors.add(:field_25, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "town or city."))
end
if field_27.blank?
if field_27.blank? && errors[:field_27].blank?
errors.add(:field_27, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 1 of postcode."))
end
if field_28.blank?
if field_28.blank? && errors[:field_28].blank?
errors.add(:field_28, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 2 of postcode."))
end
end

10
app/services/exports/export_service.rb

@ -7,7 +7,7 @@ module Exports
@logger = logger
end
def export_xml(full_update: false, collection: nil)
def export_xml(full_update: false, collection: nil, year: nil)
start_time = Time.zone.now
daily_run_number = get_daily_run_number
lettings_archives_for_manifest = {}
@ -21,12 +21,12 @@ module Exports
when "organisations"
organisations_archives_for_manifest = get_organisation_archives(start_time, full_update)
else
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, collection)
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, year)
end
else
users_archives_for_manifest = get_user_archives(start_time, full_update)
organisations_archives_for_manifest = get_organisation_archives(start_time, full_update)
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, collection)
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, year)
end
write_master_manifest(daily_run_number, lettings_archives_for_manifest.merge(users_archives_for_manifest).merge(organisations_archives_for_manifest))
@ -70,9 +70,9 @@ module Exports
organisations_export_service.export_xml_organisations(full_update:)
end
def get_lettings_archives(start_time, full_update, collection)
def get_lettings_archives(start_time, full_update, collection_year)
lettings_export_service = Exports::LettingsLogExportService.new(@storage_service, start_time)
lettings_export_service.export_xml_lettings_logs(full_update:, collection_year: collection)
lettings_export_service.export_xml_lettings_logs(full_update:, collection_year:)
end
end
end

22
app/services/exports/lettings_log_export_service.rb

@ -5,11 +5,11 @@ module Exports
def export_xml_lettings_logs(full_update: false, collection_year: nil)
archives_for_manifest = {}
collection_years_to_export(collection_year).each do |collection|
recent_export = Export.where(collection:).order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update)
archives = write_export_archive(export, collection, recent_export, full_update)
collection_years_to_export(collection_year).each do |year|
recent_export = Export.lettings.where(year:).order("started_at").last
base_number = Export.lettings.where(empty_export: false, year:).maximum(:base_number) || 1
export = build_export_run("lettings", base_number, full_update, year)
archives = write_export_archive(export, year, recent_export, full_update)
archives_for_manifest.merge!(archives)
@ -22,21 +22,21 @@ module Exports
private
def get_archive_name(collection, base_number, increment)
return unless collection
def get_archive_name(year, base_number, increment)
return unless year
base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.to_s.rjust(4, '0')}"
"core_#{collection}_#{collection + 1}_apr_mar_#{base_number_str}_#{increment_str}".downcase
"core_#{year}_#{year + 1}_apr_mar_#{base_number_str}_#{increment_str}".downcase
end
def retrieve_resources(recent_export, full_update, collection)
def retrieve_resources(recent_export, full_update, year)
if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time }
LettingsLog.exportable.where("(updated_at >= :from AND updated_at <= :to) OR (values_updated_at IS NOT NULL AND values_updated_at >= :from AND values_updated_at <= :to)", params).filter_by_year(collection)
LettingsLog.exportable.where("(updated_at >= :from AND updated_at <= :to) OR (values_updated_at IS NOT NULL AND values_updated_at >= :from AND values_updated_at <= :to)", params).filter_by_year(year)
else
params = { to: @start_time }
LettingsLog.exportable.where("updated_at <= :to", params).filter_by_year(collection)
LettingsLog.exportable.where("updated_at <= :to", params).filter_by_year(year)
end
end

12
app/services/exports/organisation_export_service.rb

@ -5,9 +5,9 @@ module Exports
def export_xml_organisations(full_update: false)
collection = "organisations"
recent_export = Export.where(collection:).order("started_at").last
recent_export = Export.organisations.order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1
base_number = Export.organisations.where(empty_export: false).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update)
archives_for_manifest = write_export_archive(export, collection, recent_export, full_update)
@ -19,15 +19,13 @@ module Exports
private
def get_archive_name(collection, base_number, increment)
return unless collection
def get_archive_name(_year, base_number, increment)
base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.to_s.rjust(4, '0')}"
"#{collection}_2024_2025_apr_mar_#{base_number_str}_#{increment_str}".downcase
"organisations_2024_2025_apr_mar_#{base_number_str}_#{increment_str}".downcase
end
def retrieve_resources(recent_export, full_update, _collection)
def retrieve_resources(recent_export, full_update, _year)
if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time }
Organisation.where("(updated_at >= :from AND updated_at <= :to)", params)

12
app/services/exports/user_export_service.rb

@ -5,9 +5,9 @@ module Exports
def export_xml_users(full_update: false)
collection = "users"
recent_export = Export.where(collection:).order("started_at").last
recent_export = Export.users.order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1
base_number = Export.users.where(empty_export: false).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update)
archives_for_manifest = write_export_archive(export, collection, recent_export, full_update)
@ -19,15 +19,13 @@ module Exports
private
def get_archive_name(collection, base_number, increment)
return unless collection
def get_archive_name(_year, base_number, increment)
base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.to_s.rjust(4, '0')}"
"#{collection}_2024_2025_apr_mar_#{base_number_str}_#{increment_str}".downcase
"users_2024_2025_apr_mar_#{base_number_str}_#{increment_str}".downcase
end
def retrieve_resources(recent_export, full_update, _collection)
def retrieve_resources(recent_export, full_update, _year)
if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time }
User.where("(updated_at >= :from AND updated_at <= :to)", params)

20
app/services/exports/xml_export_service.rb

@ -11,9 +11,9 @@ module Exports
private
def build_export_run(collection, base_number, full_update)
@logger.info("Building export run for #{collection}")
previous_exports_with_data = Export.where(collection:, empty_export: false)
def build_export_run(collection, base_number, full_update, year = nil)
@logger.info("Building export run for #{[collection, year].join(' ')}")
previous_exports_with_data = Export.where(collection:, year:, empty_export: false)
increment_number = previous_exports_with_data.where(base_number:).maximum(:increment_number) || 1
@ -25,16 +25,16 @@ module Exports
end
if previous_exports_with_data.empty?
return Export.new(collection:, base_number:, started_at: @start_time)
return Export.new(collection:, year:, base_number:, started_at: @start_time)
end
Export.new(collection:, started_at: @start_time, base_number:, increment_number:)
Export.new(collection:, year:, started_at: @start_time, base_number:, increment_number:)
end
def write_export_archive(export, collection, recent_export, full_update)
archive = get_archive_name(collection, export.base_number, export.increment_number) # archive name would be the same for all logs because they're already filtered by year (?)
def write_export_archive(export, year, recent_export, full_update)
archive = get_archive_name(year, export.base_number, export.increment_number)
initial_count = retrieve_resources(recent_export, full_update, collection).count
initial_count = retrieve_resources(recent_export, full_update, year).count
@logger.info("Creating #{archive} - #{initial_count} resources")
return {} if initial_count.zero?
@ -46,12 +46,12 @@ module Exports
loop do
slice = if last_processed_marker.present?
retrieve_resources(recent_export, full_update, collection)
retrieve_resources(recent_export, full_update, year)
.where("created_at > ?", last_processed_marker)
.order(:created_at)
.limit(MAX_XML_RECORDS).to_a
else
retrieve_resources(recent_export, full_update, collection)
retrieve_resources(recent_export, full_update, year)
.order(:created_at)
.limit(MAX_XML_RECORDS).to_a
end

23
app/views/bulk_upload_lettings_logs/forms/needstype.erb

@ -1,23 +0,0 @@
<% content_for :before_content do %>
<%= govuk_back_link href: @form.back_path %>
<% end %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "needstype"), method: :patch do |f| %>
<%= f.govuk_error_summary %>
<%= f.hidden_field :year %>
<%= f.hidden_field :organisation_id %>
<%= f.govuk_collection_radio_buttons :needstype,
@form.options,
:id,
:name,
hint: { text: I18n.t("hints.bulk_upload.needstype") },
legend: { text: "What is the needs type?", size: "l" },
caption: { text: "Upload lettings logs in bulk (#{@form.year_combo})", size: "l" } %>
<%= f.govuk_submit %>
<% end %>
</div>
</div>

21
app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb

@ -19,22 +19,17 @@
<h2 class="govuk-heading-s">Create your file</h2>
<ul class="govuk-list govuk-list--bullet">
<li>Fill in the template with data from your housing management system. Your data should go below the headers, with one row per log. Leave column A blank - the bulk upload fields start in column B.</li>
<li>Make sure each column of your data aligns with the matching headers above. You may need to reorder your data.</li>
<li>Use the <%= govuk_link_to "Lettings bulk upload Specification (2024 to 2025)", @form.specification_path %> to check your data is in the correct format.</li>
<li><strong>Username field:</strong> To assign a log to someone else, enter the email address they use to log into CORE.</li>
<li>If you have reordered the headers, keep the headers in the file.</li>
</ul>
<%= govuk_inset_text(text: "You can upload both general needs and supported housing logs in the same file for 2024 to 2025 data.") %>
<%= govuk_list [
"Fill in the template with data from your housing management system. Your data should go below the headers, with one row per log. Leave column A blank - the bulk upload fields start in column B.",
"Make sure each column of your data aligns with the matching headers above. You may need to reorder your data.",
"Use the #{govuk_link_to 'Lettings bulk upload Specification (2024 to 2025)', @form.specification_path} to check your data is in the correct format.".html_safe,
"<strong>Username field:</strong> To assign a log to someone else, enter the email address they use to log into CORE.".html_safe,
"If you have reordered the headers, keep the headers in the file.",
], type: :bullet %>
<h2 class="govuk-heading-s">Save your file</h2>
<ul class="govuk-list govuk-list--bullet">
<li>Save your file as a CSV.</li>
<li>Your file should now be ready to upload.</li>
</ul>
<%= govuk_list ["Save your file as a CSV.", "Your file should now be ready to upload."], type: :bullet %>
<%= f.govuk_submit class: "govuk-!-margin-top-7" %>
<% end %>

2
app/views/bulk_upload_lettings_resume/deletion_report.html.erb

@ -28,6 +28,6 @@
<div class="govuk-button-group">
<%= form_with model: @form, scope: :form, url: page_bulk_upload_lettings_resume_path(@bulk_upload, "confirm"), method: :patch do |f| %>
<%= f.govuk_submit "Clear this data and upload the logs" %>
<%= govuk_button_link_to "I have fixed these errors and I want to reupload the file", start_bulk_upload_lettings_logs_path, secondary: true %>
<%= govuk_button_link_to "I have fixed these errors and I want to upload the file again", start_bulk_upload_lettings_logs_path, secondary: true %>
<% end %>
</div>

12
app/views/bulk_upload_lettings_resume/fix_choice.html.erb

@ -24,17 +24,9 @@
<%= govuk_details(summary_text: "How to choose between fixing errors on the CORE site or in the CSV") do %>
<p class="govuk-body govuk-!-margin-bottom-2">You may find it easier to fix the errors in the CSV file if:</p>
<ul class="govuk-list govuk-list--bullet">
<li>you have a lot of errors</li>
<li>the CSV file is formatted incorrectly and you can see where the errors are</li>
<li>you need to fix multiple errors at once</li>
</ul>
<%= govuk_list ["you have a lot of errors", "the CSV file is formatted incorrectly and you can see where the errors are", "you need to fix multiple errors at once"], type: :bullet %>
<p class="govuk-body govuk-!-margin-bottom-2">You may find it easier to fix the errors on the CORE site if:</p>
<ul class="govuk-list govuk-list--bullet">
<li>you need to see the data in context</li>
<li>you have a smaller file, with a few errors</li>
<li>you are not sure where the errors are</li>
</ul>
<%= govuk_list ["you need to see the data in context", "you have a smaller file, with a few errors", "you are not sure where the errors are"], type: :bullet %>
<% end %>
<%= f.govuk_collection_radio_buttons :choice,

2
app/views/bulk_upload_lettings_soft_validations_check/confirm.html.erb

@ -5,7 +5,7 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<span class="govuk-caption-l">Upload lettings logs in bulk (<%= @bulk_upload.year_combo %>)</span>
<h1 class="govuk-heading-l">You have chosen to upload all logs from this bulk upload.</h1>
<h1 class="govuk-heading-l">Are you sure you want to upload all logs from this bulk upload?</h1>
<p class="govuk-body"><%= logs_and_soft_validations_warning(@bulk_upload) %></p>

19
app/views/bulk_upload_sales_logs/forms/prepare_your_file_2024.html.erb

@ -19,19 +19,16 @@
<p class="govuk-body govuk-!-margin-bottom-2">There are 8 rows of content in the templates. These rows are called the ‘headers’. They contain the CORE form questions and guidance about which questions are required and how to format your answers.</p>
<h2 class="govuk-heading-s">Create your file</h2>
<ul class="govuk-list govuk-list--bullet">
<li>Fill in the template with data from your housing management system. Your data should go below the headers, with one row per log. The bulk upload fields start at column B. Leave column A blank.</li>
<li>Make sure each column of your data aligns with the matching headers above. You may need to reorder your data.</li>
<li>Use the <%= govuk_link_to "Sales bulk upload Specification (2024 to 2025)", @form.specification_path %> to check your data is in the correct format.</li>
<li><strong>Username field:</strong> To assign a log to someone else, enter the email address they use to log into CORE.</li>
<li>If you have reordered the headers, keep the headers in the file.</li>
</ul>
<%= govuk_list [
"Fill in the template with data from your housing management system. Your data should go below the headers, with one row per log. The bulk upload fields start at column B. Leave column A blank.",
"Make sure each column of your data aligns with the matching headers above. You may need to reorder your data.",
"Use the #{govuk_link_to 'Sales bulk upload Specification (2024 to 2025)', @form.specification_path} to check your data is in the correct format.".html_safe,
"<strong>Username field:</strong> To assign a log to someone else, enter the email address they use to log into CORE.".html_safe,
"If you have reordered the headers, keep the headers in the file.",
], type: :bullet %>
<h2 class="govuk-heading-s">Save your file</h2>
<ul class="govuk-list govuk-list--bullet">
<li>Save your file as a CSV.</li>
<li>Your file should now be ready to upload.</li>
</ul>
<%= govuk_list ["Save your file as a CSV.", "Your file should now be ready to upload."], type: :bullet %>
<%= f.govuk_submit %>
<% end %>

2
app/views/bulk_upload_sales_resume/deletion_report.html.erb

@ -28,6 +28,6 @@
<div class="govuk-button-group">
<%= form_with model: @form, scope: :form, url: page_bulk_upload_sales_resume_path(@bulk_upload, "confirm"), method: :patch do |f| %>
<%= f.govuk_submit "Clear this data and upload the logs" %>
<%= govuk_button_link_to "I have fixed these errors and I want to reupload the file", start_bulk_upload_sales_logs_path, secondary: true %>
<%= govuk_button_link_to "I have fixed these errors and I want to upload the file again", start_bulk_upload_sales_logs_path, secondary: true %>
<% end %>
</div>

14
app/views/bulk_upload_sales_resume/fix_choice.html.erb

@ -23,18 +23,10 @@
</div>
<%= govuk_details(summary_text: "How to choose between fixing errors on the CORE site or in the CSV") do %>
<p class="govuk-body govuk-!-margin-bottom-2">You may find it easier to fix the errors in the CSV file if:</p>
<ul class="govuk-list govuk-list--bullet">
<li>you have a lot of errors</li>
<li>the CSV file is formatted incorrectly and you can see where the errors are</li>
<li>you need to fix multiple errors at once</li>
</ul>
<p class="govuk-body govuk-!-margin-bottom-2">You may find it easier to fix the errors in the CSV file if:</p>
<%= govuk_list ["you have a lot of errors", "the CSV file is formatted incorrectly and you can see where the errors are", "you need to fix multiple errors at once"], type: :bullet %>
<p class="govuk-body govuk-!-margin-bottom-2">You may find it easier to fix the errors on the CORE site if:</p>
<ul class="govuk-list govuk-list--bullet">
<li>you need to see the data in context</li>
<li>you have a smaller file, with a few errors</li>
<li>you are not sure where the errors are</li>
</ul>
<%= govuk_list ["you need to see the data in context", "you have a smaller file, with a few errors", "you are not sure where the errors are"], type: :bullet %>
<% end %>
<%= f.govuk_collection_radio_buttons :choice,

2
app/views/bulk_upload_sales_soft_validations_check/confirm.html.erb

@ -5,7 +5,7 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<span class="govuk-caption-l">Upload sales logs in bulk (<%= @bulk_upload.year_combo %>)</span>
<h1 class="govuk-heading-l">You have chosen to upload all logs from this bulk upload.</h1>
<h1 class="govuk-heading-l">Are you sure you want to upload all logs from this bulk upload?</h1>
<p class="govuk-body"><%= logs_and_soft_validations_warning(@bulk_upload) %></p>

4
app/views/bulk_upload_shared/_moved_user_banner.html.erb

@ -4,9 +4,9 @@
This error report is out of date.
<p>
<% if current_user.id == @bulk_upload.moved_user_id %>
You moved to a different organisation since this file was uploaded. Reupload the file to get an accurate error report.
You moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.
<% else %>
Some logs in this upload are assigned to <%= @bulk_upload.moved_user_name %>, who has moved to a different organisation since this file was uploaded. Reupload the file to get an accurate error report.
Some logs in this upload are assigned to <%= @bulk_upload.moved_user_name %>, who has moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.
<% end %>
<% end %>
<% end %>

9
app/views/bulk_upload_shared/guidance.html.erb

@ -34,12 +34,7 @@
<p class="govuk-body">The bulk upload templates contain 8 rows of ‘headers’ with information about how to fill in the template, including:</p>
<% end %>
<ul class="govuk-list govuk-list--bullet">
<li>the CORE form questions and their field numbers</li>
<li>each field’s valid responses</li>
<li>if/when certain fields can be left blank</li>
<li>which fields are used to check for duplicate logs</li>
</ul>
<%= govuk_list ["the CORE form questions and their field numbers", "each field’s valid responses", "if/when certain fields can be left blank", "which fields are used to check for duplicate logs"], type: :bullet %>
<p class="govuk-body">You can paste your data below the headers or copy the headers and insert them above the data in your file. The bulk upload fields start at column B. Leave column A blank.</p>
<p class="govuk-body">Make sure that each column of data aligns with the corresponding question in the headers. We recommend ordering your data to match the headers, but you can also reorder the headers to match your data. When processing the file, we check what each column of data represents based on the headers above.</p>
@ -80,7 +75,7 @@
<p class="govuk-body">Once you've saved your CSV file, you can upload it via a button at the top of the lettings and sales logs pages.</p>
<p class="govuk-body">When your file is done processing, you will receive an email explaining your next steps. If all your data is valid, your logs will be created. If some data is invalid, you’ll receive an email with instructions about how to resolve the errors.</p>
<p class="govuk-body">If your file has errors on fields 1 through 15 for lettings, or 1 through 18 for sales, you must fix these in the CSV. This is because we need to know these answers to validate the rest of the data. Any errors in these fields will be featured in the error report’s summary tab.</p>
<p class="govuk-body">If none of your errors are in fields 1 through 15 for lettings, or 1 through 18 for sales, you can choose how to fix the errors. You can either fix them in the CSV and reupload, or create partially complete logs and answer the remaining questions on the CORE site. Any errors that affect a significant number of logs will be featured in the error report’s summary tab to help you decide.</p>
<p class="govuk-body">If none of your errors are in fields 1 through 15 for lettings, or 1 through 18 for sales, you can choose how to fix the errors. You can either fix them in the CSV and upload the file again, or create partially complete logs and answer the remaining questions on the CORE site. Any errors that affect a significant number of logs will be featured in the error report’s summary tab to help you decide.</p>
<p class="govuk-body"></p>
<% end %>

2
app/views/bulk_upload_shared/uploads.html.erb

@ -1,4 +1,4 @@
<% item_label = format_label(@pagy.count, "uploads") %>
<% item_label = format_label(@pagy.count, "upload") %>
<% title = format_title(@searched, bulk_upload_title(controller.controller_name), current_user, item_label, @pagy.count, nil) %>
<% content_for :title, title %>

6
app/views/cookies/show.html.erb

@ -37,11 +37,7 @@
<p>Google is not allowed to use or share our analytics data with anyone.</p>
<p>Google Analytics stores anonymised information about:</p>
<ul class="govuk-list govuk-list--bullet">
<li>how you got to the service</li>
<li>the pages you visit on the service and how long you spend on them</li>
<li>any errors you see while using the service</li>
</ul>
<%= govuk_list ["how you got to the service", "the pages you visit on the service and how long you spend on them", "any errors you see while using the service"], type: :bullet %>
<table class="govuk-table">
<caption class="govuk-visually-hidden">Google Analytics cookies</caption>

6
app/views/devise/sessions/new.html.erb

@ -12,6 +12,8 @@
<%= content_for(:title) %>
</h1>
<%= render "devise/shared/links" %>
<%= f.govuk_email_field :email,
label: { text: "Email address" },
autocomplete: "email",
@ -20,10 +22,8 @@
<%= f.govuk_password_field :password,
autocomplete: "current-password" %>
<%= f.hidden_field :start, value: request["start"] %>
<%= f.hidden_field :start, value: request.params["start"] %>
<%= f.govuk_submit "Sign in" %>
</div>
</div>
<% end %>
<%= render "devise/shared/links" %>

2
app/views/devise/shared/_links.html.erb

@ -7,7 +7,7 @@
<% end %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<p class="govuk-body">You can <%= govuk_link_to "reset your password", new_password_path(resource_name) %> if you’ve forgotten it.</p>
<p class="govuk-body"><%= govuk_link_to "Forgot password", new_password_path(resource_name) %></p>
<% end %>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>

5
app/views/form/_checkbox_question.html.erb

@ -6,16 +6,17 @@
hint: { text: question.hint_text&.html_safe } do %>
<% after_divider = false %>
<% question.displayed_answer_options(@log).map do |key, option| %>
<% question.displayed_answer_options(@log).each_with_index do |(key, option), index| %>
<% if key.starts_with?("divider") %>
<% after_divider = true %>
<%= f.govuk_check_box_divider %>
<% else %>
<%= f.govuk_check_box question.id, key,
<%= f.govuk_check_box question.id.to_sym, key,
label: { text: option["value"] },
hint: { text: option["hint"] },
checked: @log[key] == 1,
exclusive: after_divider,
link_errors: index.zero? ? true : nil,
**stimulus_html_attributes(question) %>
<% end %>
<% end %>

8
app/views/form/_radio_question.html.erb

@ -18,22 +18,24 @@
legend: legend(question, page_header, conditional),
hint: { text: question.hint_text&.html_safe } do %>
<% question.displayed_answer_options(@log, current_user).map do |key, options| %>
<% question.displayed_answer_options(@log, current_user).each_with_index do |(key, options), index| %>
<% if key.starts_with?("divider") %>
<%= f.govuk_radio_divider %>
<% else %>
<% conditional_question = find_conditional_question(@page, question, key) %>
<% if conditional_question.nil? %>
<%= f.govuk_radio_button question.id,
<%= f.govuk_radio_button question.id.to_sym,
key,
label: { text: options["value"] },
hint: { text: options["hint"] },
link_errors: index.zero? ? true : nil,
**stimulus_html_attributes(question) %>
<% else %>
<%= f.govuk_radio_button question.id,
<%= f.govuk_radio_button question.id.to_sym,
key,
label: { text: options["value"] },
hint: { text: options["hint"] },
link_errors: index.zero? ? true : nil,
**stimulus_html_attributes(question) do %>
<%= render partial: "#{conditional_question.type}_question", locals: {
question: conditional_question,

3
app/views/form/page.html.erb

@ -16,7 +16,6 @@
<%= form_with model: @log, url: request.original_url, method: "post", local: true do |f| %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<% all_questions_with_errors = all_questions_affected_by_errors(@log) %>
<% remove_other_page_errors(@log, @page) %>
<%= f.govuk_error_summary %>
@ -76,7 +75,7 @@
<%= f.hidden_field :check_errors, value: @check_errors %>
<% end %>
<% if all_questions_with_errors.count > 1 %>
<% if @pages_with_errors_count > 1 %>
<div class="govuk-button-group">
<%= f.submit "See all related answers", name: "check_errors", class: "govuk-body govuk-link submit-button-link" %>
</div>

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

@ -103,7 +103,7 @@
<% feedback_link = govuk_link_to "giving us your feedback (opens in a new tab)", t("feedback_form"), rel: "noreferrer noopener", target: "_blank" %>
<%= govuk_phase_banner(
classes: "govuk-width-container",
classes: "#{current_user.present? ? 'no-bottom-border ' : ''}govuk-width-container",
tag: govuk_phase_banner_tag(current_user),
text: "This is a new service – help us improve it by #{feedback_link}".html_safe,
) %>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save