Browse Source

Merge branch 'main' into CLDC-1226-deactivate-users

pull/624/head
Kat 4 years ago
parent
commit
e8c32b1ea9
  1. 2
      .ruby-version
  2. 6
      Dockerfile
  3. 4
      Dockerfile_dev
  4. 5
      Gemfile
  5. 202
      Gemfile.lock
  6. 37
      app/admin/admin_users.rb
  7. 20
      app/admin/case_logs.rb
  8. 32
      app/admin/dashboard.rb
  9. 31
      app/admin/organisations.rb
  10. 42
      app/admin/users.rb
  11. 4
      app/components/search_component.rb
  12. 0
      app/concerns/.keep
  13. 15
      app/concerns/admin/paper_trail.rb
  14. 6
      app/controllers/auth/passwords_controller.rb
  15. 20
      app/controllers/auth/sessions_controller.rb
  16. 14
      app/controllers/case_logs_controller.rb
  17. 7
      app/controllers/organisations_controller.rb
  18. 7
      app/frontend/active_admin.js
  19. 2
      app/frontend/styles/_table-group.scss
  20. 17
      app/frontend/styles/active_admin.scss
  21. 5
      app/helpers/organisation_helper.rb
  22. 36
      app/models/admin_user.rb
  23. 11
      app/models/case_log.rb
  24. 8
      app/models/organisation.rb
  25. 7
      app/models/user.rb
  26. 2
      app/services/exports/case_log_export_service.rb
  27. 35
      app/services/imports/case_logs_field_import_service.rb
  28. 6
      app/services/imports/case_logs_import_service.rb
  29. 1
      app/services/imports/import_service.rb
  30. 6
      app/views/case_logs/_log_list.html.erb
  31. 29
      app/views/case_logs/index.html.erb
  32. 5
      app/views/layouts/organisations.html.erb
  33. 60
      app/views/organisations/_organisation_list.html.erb
  34. 28
      app/views/organisations/logs.html.erb
  35. 2
      app/views/organisations/show.html.erb
  36. 96
      app/views/users/_user_list.html.erb
  37. 1
      config/environments/test.rb
  38. 339
      config/initializers/active_admin.rb
  39. 2
      config/initializers/devise.rb
  40. 25
      config/routes.rb
  41. 36
      db/migrate/20220525130518_drop_admin_users.rb
  42. 31
      db/schema.rb
  43. 18
      lib/tasks/data_import_field.rake
  44. 1
      package.json
  45. 78
      spec/controllers/admin/admin_users_controller_spec.rb
  46. 92
      spec/controllers/admin/case_logs_controller_spec.rb
  47. 44
      spec/controllers/admin/dashboard_controller_spec.rb
  48. 78
      spec/controllers/admin/organisations_controller_spec.rb
  49. 97
      spec/controllers/admin/users_controller_spec.rb
  50. 18
      spec/factories/case_log.rb
  51. 162
      spec/features/admin_panel_spec.rb
  52. 50
      spec/features/auth/user_lockout_spec.rb
  53. 2
      spec/features/form/saving_data_spec.rb
  54. 59
      spec/features/log_spec.rb
  55. 47
      spec/features/organisation_spec.rb
  56. 2
      spec/fixtures/exports/case_logs.csv
  57. 2
      spec/fixtures/exports/case_logs.xml
  58. 514
      spec/fixtures/softwire_imports/case_logs/5ybz29dj-l33t-k1l0-hj86-n4k4ma77xkcd.xml
  59. 514
      spec/fixtures/softwire_imports/case_logs/893ufj2s-lq77-42m4-rty6-ej09gh585uy1.xml
  60. 9
      spec/lib/tasks/data_import_spec.rb
  61. 57
      spec/lib/tasks/date_import_field_spec.rb
  62. 84
      spec/models/admin_user_spec.rb
  63. 96
      spec/models/case_log_spec.rb
  64. 12
      spec/models/user_spec.rb
  65. 72
      spec/requests/auth/passwords_controller_spec.rb
  66. 147
      spec/requests/case_logs_controller_spec.rb
  67. 111
      spec/requests/organisations_controller_spec.rb
  68. 41
      spec/requests/rack_attack_spec.rb
  69. 4
      spec/services/exports/case_log_export_service_spec.rb
  70. 76
      spec/services/imports/case_logs_field_import_service_spec.rb
  71. 37
      spec/services/imports/case_logs_import_service_spec.rb
  72. 2
      spec/services/imports/organisation_la_import_service_spec.rb
  73. 2
      spec/services/imports/organisation_rent_period_import_service_spec.rb
  74. 10
      webpack.config.js
  75. 897
      yarn.lock

2
.ruby-version

@ -1 +1 @@
3.1.1
3.1.2

6
Dockerfile

@ -1,5 +1,5 @@
# Build compilation image
FROM ruby:3.1.1-alpine as builder
FROM ruby:3.1.2-alpine as builder
# The application runs from /app
WORKDIR /app
@ -16,7 +16,7 @@ RUN apk add --no-cache build-base yarn postgresql-dev git
# Install bundler to run bundle exec
# This should be the same version as the Gemfile.lock
RUN gem install bundler:2.3.7 --no-document
RUN gem install bundler:2.3.14 --no-document
# Install gems defined in Gemfile
COPY .ruby-version Gemfile Gemfile.lock /app/
@ -45,7 +45,7 @@ RUN rm -rf node_modules log tmp && \
find /usr/local/bundle/gems -name "*.html" -delete
# Build runtime image
FROM ruby:3.1.1-alpine as production
FROM ruby:3.1.2-alpine as production
# The application runs from /app
WORKDIR /app

4
Dockerfile_dev

@ -1,5 +1,5 @@
# Build compilation image
FROM ruby:3.1.1-alpine
FROM ruby:3.1.2-alpine
# The application runs from /app
WORKDIR /app
@ -11,7 +11,7 @@ RUN apk add --no-cache build-base yarn postgresql-dev git bash
# Install bundler to run bundle exec
# This should be the same version as the Gemfile.lock
RUN gem install bundler:2.3.4 --no-document
RUN gem install bundler:2.3.14 --no-document
# Install gems defined in Gemfile
COPY .ruby-version Gemfile Gemfile.lock /app/

5
Gemfile

@ -3,7 +3,7 @@
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.1.1"
ruby "3.1.2"
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
gem "rails", "~> 7.0.2"
@ -27,8 +27,6 @@ gem "govuk_markdown"
gem "notifications-ruby-client"
# A modest javascript framework for the html you already have
gem "stimulus-rails"
# Administration framework
gem "activeadmin"
# Admin charts
gem "chartkick"
# Spreadsheet parsing
@ -89,6 +87,7 @@ group :test do
gem "capybara", require: false
gem "capybara-lockstep"
gem "factory_bot_rails"
gem "faker"
gem "rspec-rails", require: false
gem "selenium-webdriver", require: false
gem "simplecov", require: false

202
Gemfile.lock

@ -24,94 +24,82 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.2.4)
actionpack (= 7.0.2.4)
activesupport (= 7.0.2.4)
actioncable (7.0.3)
actionpack (= 7.0.3)
activesupport (= 7.0.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.2.4)
actionpack (= 7.0.2.4)
activejob (= 7.0.2.4)
activerecord (= 7.0.2.4)
activestorage (= 7.0.2.4)
activesupport (= 7.0.2.4)
actionmailbox (7.0.3)
actionpack (= 7.0.3)
activejob (= 7.0.3)
activerecord (= 7.0.3)
activestorage (= 7.0.3)
activesupport (= 7.0.3)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.2.4)
actionpack (= 7.0.2.4)
actionview (= 7.0.2.4)
activejob (= 7.0.2.4)
activesupport (= 7.0.2.4)
actionmailer (7.0.3)
actionpack (= 7.0.3)
actionview (= 7.0.3)
activejob (= 7.0.3)
activesupport (= 7.0.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.2.4)
actionview (= 7.0.2.4)
activesupport (= 7.0.2.4)
actionpack (7.0.3)
actionview (= 7.0.3)
activesupport (= 7.0.3)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.2.4)
actionpack (= 7.0.2.4)
activerecord (= 7.0.2.4)
activestorage (= 7.0.2.4)
activesupport (= 7.0.2.4)
actiontext (7.0.3)
actionpack (= 7.0.3)
activerecord (= 7.0.3)
activestorage (= 7.0.3)
activesupport (= 7.0.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.2.4)
activesupport (= 7.0.2.4)
actionview (7.0.3)
activesupport (= 7.0.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activeadmin (2.12.0)
arbre (~> 1.2, >= 1.2.1)
formtastic (>= 3.1, < 5.0)
formtastic_i18n (~> 0.4)
inherited_resources (~> 1.7)
jquery-rails (~> 4.2)
kaminari (~> 1.0, >= 1.2.1)
railties (>= 6.0, < 7.1)
ransack (>= 2.1.1, < 4)
activejob (7.0.2.4)
activesupport (= 7.0.2.4)
activejob (7.0.3)
activesupport (= 7.0.3)
globalid (>= 0.3.6)
activemodel (7.0.2.4)
activesupport (= 7.0.2.4)
activerecord (7.0.2.4)
activemodel (= 7.0.2.4)
activesupport (= 7.0.2.4)
activestorage (7.0.2.4)
actionpack (= 7.0.2.4)
activejob (= 7.0.2.4)
activerecord (= 7.0.2.4)
activesupport (= 7.0.2.4)
activemodel (7.0.3)
activesupport (= 7.0.3)
activerecord (7.0.3)
activemodel (= 7.0.3)
activesupport (= 7.0.3)
activestorage (7.0.3)
actionpack (= 7.0.3)
activejob (= 7.0.3)
activerecord (= 7.0.3)
activesupport (= 7.0.3)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.2.4)
activesupport (7.0.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
arbre (1.5.0)
activesupport (>= 3.0.0, < 7.1)
ruby2_keywords (>= 0.0.2, < 1.0)
ast (2.4.2)
aws-eventstream (1.2.0)
aws-partitions (1.584.0)
aws-sdk-core (3.130.2)
aws-partitions (1.592.0)
aws-sdk-core (3.131.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.56.0)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.57.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
@ -120,7 +108,7 @@ GEM
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.0)
aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.17)
bcrypt (3.1.18)
better_html (1.0.16)
actionview (>= 4.0)
activesupport (>= 4.0)
@ -133,11 +121,11 @@ GEM
bootsnap (1.11.1)
msgpack (~> 1.2)
builder (3.2.4)
bundler-audit (0.9.0.1)
bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
byebug (11.1.3)
capybara (3.36.0)
capybara (3.37.1)
addressable
matrix
mini_mime (>= 0.1.3)
@ -158,7 +146,6 @@ GEM
crack (0.4.5)
rexml
crass (1.0.6)
deep_merge (1.2.2)
diff-lcs (1.5.0)
digest (3.1.0)
docile (1.4.0)
@ -182,63 +169,40 @@ GEM
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faker (2.21.0)
i18n (>= 1.8.11, < 2)
ffi (1.15.5)
formtastic (4.0.0)
actionpack (>= 5.2.0)
formtastic_i18n (0.7.0)
globalid (1.0.0)
activesupport (>= 5.0)
govuk-components (3.0.3)
govuk-components (3.0.4)
activemodel (>= 6.1)
railties (>= 6.1)
view_component (~> 2.49.1)
govuk_design_system_formbuilder (3.0.2)
govuk_design_system_formbuilder (3.0.3)
actionview (>= 6.1)
activemodel (>= 6.1)
activesupport (>= 6.1)
deep_merge (~> 1.2.1)
html-attributes-utils (~> 0.9.0)
govuk_markdown (1.0.0)
activesupport
redcarpet
has_scope (0.8.0)
actionpack (>= 5.2)
activesupport (>= 5.2)
hashdiff (1.0.1)
html-attributes-utils (0.9.0)
activesupport (>= 6.1.4.4)
html_tokenizer (0.0.7)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
inherited_resources (1.13.1)
actionpack (>= 5.2, < 7.1)
has_scope (~> 0.6)
railties (>= 5.2, < 7.1)
responders (>= 2, < 4)
iniparse (1.5.0)
jmespath (1.6.1)
jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
jsbundling-rails (1.0.2)
railties (>= 6.0.0)
json-schema (3.0.0)
addressable (>= 2.8)
jwt (2.3.0)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
kaminari-activerecord (= 1.2.2)
kaminari-core (= 1.2.2)
kaminari-actionview (1.2.2)
actionview
kaminari-core (= 1.2.2)
kaminari-activerecord (1.2.2)
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.17.0)
loofah (2.18.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -306,35 +270,35 @@ GEM
puma (5.6.4)
nio4r (~> 2.0)
racc (1.6.0)
rack (2.2.3)
rack (2.2.3.1)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack-mini-profiler (2.3.4)
rack (>= 1.2.0)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (7.0.2.4)
actioncable (= 7.0.2.4)
actionmailbox (= 7.0.2.4)
actionmailer (= 7.0.2.4)
actionpack (= 7.0.2.4)
actiontext (= 7.0.2.4)
actionview (= 7.0.2.4)
activejob (= 7.0.2.4)
activemodel (= 7.0.2.4)
activerecord (= 7.0.2.4)
activestorage (= 7.0.2.4)
activesupport (= 7.0.2.4)
rails (7.0.3)
actioncable (= 7.0.3)
actionmailbox (= 7.0.3)
actionmailer (= 7.0.3)
actionpack (= 7.0.3)
actiontext (= 7.0.3)
actionview (= 7.0.3)
activejob (= 7.0.3)
activemodel (= 7.0.3)
activerecord (= 7.0.3)
activestorage (= 7.0.3)
activesupport (= 7.0.3)
bundler (>= 1.15.0)
railties (= 7.0.2.4)
railties (= 7.0.3)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.2)
loofah (~> 2.3)
railties (7.0.2.4)
actionpack (= 7.0.2.4)
activesupport (= 7.0.2.4)
railties (7.0.3)
actionpack (= 7.0.3)
activesupport (= 7.0.3)
method_source
rake (>= 12.2)
thor (~> 1.0)
@ -342,16 +306,12 @@ GEM
rainbow (3.1.1)
rake (13.0.6)
randexp (0.1.7)
ransack (3.1.0)
activerecord (>= 6.0.4)
activesupport (>= 6.0.4)
i18n
rb-fsevent (0.11.1)
rb-inotify (0.10.1)
ffi (~> 1.0)
redcarpet (3.5.1)
redis (4.6.0)
regexp_parser (2.3.1)
regexp_parser (2.4.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.0.1)
@ -396,7 +356,7 @@ GEM
rubocop-rails (= 2.13.2)
rubocop-rake (= 0.6.0)
rubocop-rspec (= 2.7.0)
rubocop-performance (1.13.3)
rubocop-performance (1.14.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.13.2)
@ -414,13 +374,13 @@ GEM
childprocess (>= 0.5, < 5.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2)
sentry-rails (5.3.0)
sentry-rails (5.3.1)
railties (>= 5.0)
sentry-ruby-core (~> 5.3.0)
sentry-ruby (5.3.0)
sentry-ruby-core (~> 5.3.1)
sentry-ruby (5.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-ruby-core (= 5.3.0)
sentry-ruby-core (5.3.0)
sentry-ruby-core (= 5.3.1)
sentry-ruby-core (5.3.1)
concurrent-ruby
simplecov (0.21.2)
docile (~> 1.1)
@ -431,10 +391,10 @@ GEM
smart_properties (1.17.0)
stimulus-rails (1.0.4)
railties (>= 6.0.0)
strscan (3.0.1)
strscan (3.0.3)
thor (1.2.1)
timecop (0.9.5)
timeout (0.2.0)
timeout (0.3.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
uk_postcode (2.1.7)
@ -468,7 +428,6 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
activeadmin
aws-sdk-s3
bootsnap (>= 1.4.4)
bundler-audit
@ -480,6 +439,7 @@ DEPENDENCIES
dotenv-rails
erb_lint
factory_bot_rails
faker
govuk-components
govuk_design_system_formbuilder
govuk_markdown
@ -519,7 +479,7 @@ DEPENDENCIES
webmock
RUBY VERSION
ruby 3.1.1p18
ruby 3.1.2p20
BUNDLED WITH
2.3.12
2.3.14

37
app/admin/admin_users.rb

@ -1,37 +0,0 @@
ActiveAdmin.register AdminUser do
permit_params :email, :phone, :password, :password_confirmation
controller do
def update_resource(object, attributes)
update_method = attributes.first[:password].present? ? :update : :update_without_password
object.send(update_method, *attributes)
end
end
index do
selectable_column
id_column
column :email
column "Phone Number", :phone
column :current_sign_in_at
column :sign_in_count
column :created_at
actions
end
filter :email
filter :phone
filter :current_sign_in_at
filter :sign_in_count
filter :created_at
form do |f|
f.inputs do
f.input :email
f.input :phone
f.input :password
f.input :password_confirmation
end
f.actions
end
end

20
app/admin/case_logs.rb

@ -1,20 +0,0 @@
ActiveAdmin.register CaseLog do
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
permit_params do
CaseLog.editable_fields
end
index do
selectable_column
id_column
column :created_at
column :updated_at
column :status
column :tenant_code
column :postcode_full
column :owning_organisation
column :managing_organisation
actions
end
end

32
app/admin/dashboard.rb

@ -1,32 +0,0 @@
ActiveAdmin.register_page "Dashboard" do
menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }
content title: proc { I18n.t("active_admin.dashboard") } do
columns do
column do
panel "Recent logs" do
table_for CaseLog.order(updated_at: :desc).limit(10) do
column :id
column :created_at
column :updated_at
column :status
column :tenant_code
column :postcode_full
end
end
end
column do
panel "Total logs in progress" do
para CaseLog.in_progress.size
end
panel "Total logs completed" do
para CaseLog.completed.size
end
panel "Total logs completed" do
pie_chart CaseLog.group(:status).size
end
end
end
end
end

31
app/admin/organisations.rb

@ -1,31 +0,0 @@
ActiveAdmin.register Organisation do
permit_params do
permitted = %i[name
phone
provider_type
address_line1
address_line2
postcode
local_authorities
holds_own_stock
other_stock_owners
managing_agents]
permitted
end
index do
selectable_column
id_column
column :name
column "Org type", :provider_type
column "Address Line 1", :address_line1
column "Address Line 2", :address_line2
column :postcode
column "Phone Number", :phone
column :local_authorities
column :holds_own_stock
column :other_stock_owners
column :managing_agents
actions
end
end

42
app/admin/users.rb

@ -1,42 +0,0 @@
ActiveAdmin.register User do
permit_params :name, :email, :password, :password_confirmation, :organisation_id, :role
controller do
def update_resource(object, attributes)
update_method = attributes.first[:password].present? ? :update : :update_without_password
object.send(update_method, *attributes)
end
end
index do
selectable_column
id_column
column :name
column :email
column :organisation
column(:role) { |u| u.role.to_s.humanize }
column :current_sign_in_at
column :sign_in_count
column :created_at
actions
end
filter :email
filter :name
filter :organisation
filter :current_sign_in_at
filter :sign_in_count
filter :created_at
form do |f|
f.inputs do
f.input :name
f.input :email
f.input :password
f.input :password_confirmation
f.input :organisation
f.input :role
end
f.actions
end
end

4
app/components/search_component.rb

@ -11,8 +11,12 @@ class SearchComponent < ViewComponent::Base
def path(current_user)
if request.path.include?("users")
user_path(current_user)
elsif request.path.include?("organisations") && request.path.include?("logs")
request.path
elsif request.path.include?("organisations")
organisations_path
elsif request.path.include?("logs")
case_logs_path
end
end

0
app/concerns/.keep

15
app/concerns/admin/paper_trail.rb

@ -1,15 +0,0 @@
module Admin
module PaperTrail
extend ActiveSupport::Concern
included do
before_action :set_paper_trail_whodunnit
end
protected
def user_for_paper_trail
current_admin_user
end
end
end

6
app/controllers/auth/passwords_controller.rb

@ -62,12 +62,8 @@ protected
resource.need_two_factor_authentication?(request) ? :updated_2FA : :updated
end
def resource_class_name
resource_class.name.underscore
end
def after_sending_reset_password_instructions_path_for(_resource)
account_password_reset_confirmation_path(email: params.dig(resource_class_name, "email"))
account_password_reset_confirmation_path(email: params.dig("user", "email"))
end
def after_resetting_password_path_for(resource)

20
app/controllers/auth/sessions_controller.rb

@ -2,13 +2,13 @@ class Auth::SessionsController < Devise::SessionsController
include Helpers::Email
def create
self.resource = resource_class.new
if params.dig(resource_class_name, "email").empty?
self.resource = User.new
if params.dig("user", "email").empty?
resource.errors.add :email, "Enter an email address"
elsif !email_valid?(params.dig(resource_class_name, "email"))
elsif !email_valid?(params.dig("user", "email"))
resource.errors.add :email, "Enter an email address in the correct format, like name@example.com"
end
if params.dig(resource_class_name, "password").empty?
if params.dig("user", "password").empty?
resource.errors.add :password, "Enter a password"
end
if resource.errors.present?
@ -20,19 +20,11 @@ class Auth::SessionsController < Devise::SessionsController
private
def resource_class
request.path.include?("admin") ? AdminUser : User
end
def resource_class_name
resource_class.name.underscore
end
def after_sign_in_path_for(resource)
if resource.need_two_factor_authentication?(request)
send("#{resource_name}_two_factor_authentication_path")
user_two_factor_authentication_path
else
params.dig(resource_class_name, "start").present? ? case_logs_path : super
params.dig("user", "start").present? ? case_logs_path : super
end
end
end

14
app/controllers/case_logs_controller.rb

@ -1,6 +1,7 @@
class CaseLogsController < ApplicationController
include Pagy::Backend
include Modules::CaseLogsFilter
include Modules::SearchFilter
skip_before_action :verify_authenticity_token, if: :json_api_request?
before_action :authenticate, if: :json_api_request?
@ -10,12 +11,17 @@ class CaseLogsController < ApplicationController
def index
set_session_filters
@pagy, @case_logs = pagy(filtered_case_logs(current_user.case_logs))
all_logs = current_user.case_logs
unpaginated_filtered_logs = filtered_case_logs(filtered_collection(all_logs, search_term))
@pagy, @case_logs = pagy(unpaginated_filtered_logs)
@searched = search_term.presence
@total_count = all_logs.size
respond_to do |format|
format.html
format.csv do
send_data filtered_case_logs(current_user.case_logs).to_csv, filename: "logs-#{Time.zone.now}.csv"
send_data unpaginated_filtered_logs.to_csv, filename: "logs-#{Time.zone.now}.csv"
end
end
end
@ -85,6 +91,10 @@ private
API_ACTIONS = %w[create show update destroy].freeze
def search_term
params["search"]
end
def json_api_request?
API_ACTIONS.include?(request["action"]) && request.format.json?
end

7
app/controllers/organisations_controller.rb

@ -55,7 +55,12 @@ class OrganisationsController < ApplicationController
set_session_filters(specific_org: true)
organisation_logs = CaseLog.all.where(owning_organisation_id: @organisation.id)
@pagy, @case_logs = pagy(filtered_case_logs(organisation_logs))
unpaginated_filtered_logs = filtered_case_logs(filtered_collection(organisation_logs, search_term))
@pagy, @case_logs = pagy(unpaginated_filtered_logs)
@searched = search_term.presence
@total_count = organisation_logs.size
render "logs", layout: "application"
else
redirect_to(case_logs_path)

7
app/frontend/active_admin.js

@ -1,7 +0,0 @@
// Load Active Admin's styles into Webpacker,
// see `active_admin.scss` for customization.
import "./styles/active_admin.scss";
import "@activeadmin/activeadmin";
import "chartkick/chart.js"

2
app/frontend/styles/_table-group.scss

@ -2,7 +2,7 @@
overflow-x: auto;
overflow-y: hidden;
margin: govuk-spacing(-3) govuk-spacing(-3) govuk-spacing(3);
padding: 0 govuk-spacing(3);
padding: govuk-spacing(3) govuk-spacing(3);
scrollbar-color: $govuk-text-colour govuk-colour("light-grey");
.govuk-table {

17
app/frontend/styles/active_admin.scss

@ -1,17 +0,0 @@
// Sass variable overrides must be declared before loading up Active Admin's styles.
//
// To view the variables that Active Admin provides, take a look at
// `app/assets/stylesheets/active_admin/mixins/_variables.scss` in the
// Active Admin source.
//
// For example, to change the sidebar width:
// $sidebar-width: 242px;
// Active Admin's got SASS!
@import "@activeadmin/activeadmin/src/scss/mixins";
@import "@activeadmin/activeadmin/src/scss/base";
// Overriding any non-variable Sass must be done after the fact.
// For example, to change the default status-tag color:
//
// .status_tag { background: #6090DB; }

5
app/helpers/organisation_helper.rb

@ -8,9 +8,4 @@ module OrganisationHelper
current_organisation.name
end
end
DISPLAY_PROVIDER_TYPE = { "LA": "Local authority", "PRP": "Private registered provider" }.freeze
def display_provider_type(provider_type)
DISPLAY_PROVIDER_TYPE[provider_type.to_sym]
end
end

36
app/models/admin_user.rb

@ -1,36 +0,0 @@
class AdminUser < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :timeoutable, :omniauthable
devise :two_factor_authenticatable, :database_authenticatable, :recoverable,
:rememberable, :validatable, :trackable, :lockable
has_one_time_password(encrypted: true)
has_paper_trail ignore: %w[last_sign_in_at
current_sign_in_at
current_sign_in_ip
last_sign_in_ip
failed_attempts
unlock_token
locked_at
reset_password_token
reset_password_sent_at
remember_created_at
sign_in_count
updated_at]
validates :phone, presence: true, numericality: true
MFA_TEMPLATE_ID = "6bdf5ee1-8e01-4be1-b1f9-747061d8a24c".freeze
RESET_PASSWORD_TEMPLATE_ID = "fbb2d415-b9b1-4507-ba0a-6e542fa3504d".freeze
def send_two_factor_authentication_code(code)
template_id = MFA_TEMPLATE_ID
personalisation = { otp: code }
DeviseNotifyMailer.new.send_email(email, template_id, personalisation)
end
def reset_password_notify_template
RESET_PASSWORD_TEMPLATE_ID
end
end

11
app/models/case_log.rb

@ -51,6 +51,17 @@ class CaseLog < ApplicationRecord
end
}
scope :filter_by_id, ->(id) { where(id:) }
scope :filter_by_tenant_code, ->(tenant_code) { where("tenant_code ILIKE ?", "%#{tenant_code}%") }
scope :filter_by_propcode, ->(propcode) { where("propcode ILIKE ?", "%#{propcode}%") }
scope :filter_by_postcode, ->(postcode_full) { where("postcode_full ILIKE ?", "%#{postcode_full.gsub(/\s+/, '')}%") }
scope :search_by, lambda { |param|
filter_by_id(param)
.or(filter_by_tenant_code(param))
.or(filter_by_propcode(param))
.or(filter_by_postcode(param))
}
AUTOGENERATED_FIELDS = %w[id status created_at updated_at discarded_at].freeze
OPTIONAL_FIELDS = %w[first_time_property_let_as_social_housing tenant_code propcode].freeze
RENT_TYPE_MAPPING = { 0 => 1, 1 => 2, 2 => 2, 3 => 3, 4 => 3, 5 => 3 }.freeze

8
app/models/organisation.rb

@ -62,12 +62,18 @@ class Organisation < ApplicationRecord
data_protection_confirmed? ? "Accepted" : "Not accepted"
end
DISPLAY_PROVIDER_TYPE = { "LA": "Local authority", "PRP": "Private registered provider" }.freeze
def display_provider_type
DISPLAY_PROVIDER_TYPE[provider_type.to_sym]
end
def display_attributes
[
{ name: "name", value: name, editable: true },
{ name: "address", value: address_string, editable: true },
{ name: "telephone_number", value: phone, editable: true },
{ name: "type", value: "Org type", editable: false },
{ name: "type", value: display_provider_type, editable: false },
{ name: "local_authorities_operated_in", value: local_authority_names, editable: false, format: :bullet },
{ name: "rent_periods", value: rent_period_labels, editable: false, format: :bullet },
{ name: "holds_own_stock", value: holds_own_stock.to_s.humanize, editable: false },

7
app/models/user.rb

@ -34,6 +34,7 @@ class User < ApplicationRecord
scope :search_by_name, ->(name) { where("name ILIKE ?", "%#{name}%") }
scope :search_by_email, ->(email) { where("email ILIKE ?", "%#{email}%") }
scope :filter_by_active, -> { where(active: true) }
scope :search_by, ->(param) { search_by_name(param).or(search_by_email(param)) }
def case_logs
@ -89,8 +90,10 @@ class User < ApplicationRecord
old_user_id.present?
end
def skip_confirmation!
!active?
def send_confirmation_instructions
return unless active?
super
end
def need_two_factor_authentication?(_request)

2
app/services/exports/case_log_export_service.rb

@ -179,7 +179,7 @@ module Exports
# Mapping which would require a change in our data model
attribute_hash["createddate"] = attribute_hash["created_at"]
attribute_hash["uploaddate"] = attribute_hash["updated_at"]
attribute_hash["tenancycode"] = attribute_hash["tenancy_code"]
attribute_hash["tenancycode"] = attribute_hash["tenant_code"]
attribute_hash["ppcodenk"] = attribute_hash["previous_postcode_known"]
# Age refused

35
app/services/imports/case_logs_field_import_service.rb

@ -0,0 +1,35 @@
module Imports
class CaseLogsFieldImportService < ImportService
def update_field(field, folder)
case field
when "tenant_code"
import_from(folder, :update_tenant_code)
else
raise "Updating #{field} is not supported by the field import service"
end
end
private
def update_tenant_code(xml_doc)
old_id = field_value(xml_doc, "meta", "document-id")
record = CaseLog.find_by(old_id:)
if record.present?
tenant_code = string_or_nil(xml_doc, "_2bTenCode")
if tenant_code.present? && record.tenant_code.blank?
record.update!(tenant_code:)
else
@logger.info("Case Log #{record.id} has a value for tenant_code, skipping update")
end
else
@logger.warn("Could not find record matching legacy ID #{old_id}")
end
end
def string_or_nil(xml_doc, attribute)
str = field_value(xml_doc, "xmlns", attribute)
str.presence
end
end
end

6
app/services/imports/case_logs_import_service.rb

@ -2,6 +2,9 @@ module Imports
class CaseLogsImportService < ImportService
def create_logs(folder)
import_from(folder, :create_log)
if @logs_with_discrepancies.count.positive?
@logger.warn("The following case logs had status discrepancies: [#{@logs_with_discrepancies.join(', ')}]")
end
end
private
@ -52,6 +55,7 @@ module Imports
attributes["joint"] = unsafe_string_as_integer(xml_doc, "joint")
attributes["startertenancy"] = unsafe_string_as_integer(xml_doc, "_2a")
attributes["tenancy"] = unsafe_string_as_integer(xml_doc, "Q2b")
attributes["tenant_code"] = string_or_nil(xml_doc, "_2bTenCode")
attributes["tenancyother"] = string_or_nil(xml_doc, "Q2ba")
attributes["tenancylength"] = safe_string_as_integer(xml_doc, "_2cYears")
attributes["needstype"] = needs_type(xml_doc)
@ -225,6 +229,8 @@ module Imports
def check_status_completed(case_log, previous_status)
if previous_status.include?("submitted") && case_log.status != "completed"
@logger.warn "Case log #{case_log.id} is not completed"
@logger.warn "Case log with old id:#{case_log.old_id} is incomplete but status should be complete"
@logs_with_discrepancies << case_log.old_id
end
end

1
app/services/imports/import_service.rb

@ -5,6 +5,7 @@ module Imports
def initialize(storage_service, logger = Rails.logger)
@storage_service = storage_service
@logger = logger
@logs_with_discrepancies = []
end
def import_from(folder, create_method)

6
app/views/case_logs/_log_list.html.erb

@ -1,8 +1,12 @@
<section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>">
<%= govuk_table do |table| %>
<%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular], id: title.dasherize) do |caption| %>
<span class="govuk-!-margin-right-4">
<span class="govuk-!-margin-right-4">
<% if defined?(searched) && searched.present? %>
<strong><%= pagy.count %></strong> <%= item_label %> found matching ‘<%= searched %>’ of <strong><%= total_count %></strong> total <%= title.downcase %>. <%= govuk_link_to("Clear search", request.path) %>
<% else %>
<strong><%= pagy.count %></strong> total <%= title.downcase %>
<% end %>
</span>
<%= govuk_link_to "Download (CSV)", "/logs.csv", type: "text/csv" %>
<% end %>

29
app/views/case_logs/index.html.erb

@ -1,8 +1,17 @@
<% content_for :title, "Logs" %>
<% item_label = @pagy.count > 1 ? "logs" : "log" %>
<% if @searched.present? %>
<% title = "Your organisation (#{@pagy.count} #{item_label} matching ‘#{@searched}’ of #{@total_count} total logs)" %>
<% else %>
<% title = "Your organisation (Logs)" %>
<% end %>
<% content_for :title, title %>
<% content_for :tab_title do %>
<%= "Logs" %>
<% end %>
<h1 class="govuk-heading-l">
<%= content_for(:title) %>
</h1>
<div class="app-filter-layout" data-controller="filter-layout">
<div class="govuk-button-group app-filter-toggle">
<%= govuk_button_to "Create a new lettings log", case_logs_path %>
@ -10,10 +19,10 @@
</div>
<%= render partial: "log_filters" %>
<% if @case_logs.present? %>
<div class="app-filter-layout__content">
<%= render partial: "log_list", locals: { case_logs: @case_logs, title: "Logs", pagy: @pagy } %>
<%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "logs" } %>
</div>
<% end %>
<div class="app-filter-layout__content">
<%= render SearchComponent.new(current_user:, search_label: "Search by log ID, tenant code, property reference or postcode", value: @searched) %>
<hr class="govuk-section-break govuk-section-break--visible govuk-section-break--m">
<%= render partial: "log_list", locals: { case_logs: @case_logs, title: "Logs", pagy: @pagy, searched: @searched, item_label:, total_count: @total_count } %>
<%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "logs" } %>
</div>
</div>

5
app/views/layouts/organisations.html.erb

@ -1,6 +1,9 @@
<% content_for :content do %>
<h1 class="govuk-heading-l">
<%= organisation_header(request.path, current_user, @organisation) %>
<% if current_user.support? && request.path != "/organisations" %>
<span class="govuk-caption-l"><%= @organisation.name %></span>
<% end %>
<%= content_for(:title) %>
</h1>
<% items = tab_items(current_user) %>

60
app/views/organisations/_organisation_list.html.erb

@ -1,35 +1,37 @@
<%= govuk_table do |table| %>
<%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<% if searched.present? %>
<strong><%= pagy.count %></strong> <%= item_label %> found matching ‘<%= searched %>’ of <strong><%= total_count %></strong> total organisations. <%= govuk_link_to("Clear search", request.path) %>
<% else %>
<strong><%= pagy.count %></strong> total organisations.
<section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>">
<%= govuk_table do |table| %>
<%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<% if searched.present? %>
<strong><%= pagy.count %></strong> <%= item_label %> found matching ‘<%= searched %>’ of <strong><%= total_count %></strong> total organisations. <%= govuk_link_to("Clear search", request.path) %>
<% else %>
<strong><%= pagy.count %></strong> total organisations.
<% end %>
<% end %>
<% end %>
<%= table.head do |head| %>
<%= head.row do |row| %>
<% row.cell(header: true, text: "Name", html_attributes: {
scope: "col",
}) %>
<% row.cell(header: true, text: "Registration number", html_attributes: {
scope: "col",
}) %>
<% row.cell(header: true, text: "Type", html_attributes: {
scope: "col",
}) %>
<%= table.head do |head| %>
<%= head.row do |row| %>
<% row.cell(header: true, text: "Name", html_attributes: {
scope: "col",
}) %>
<% row.cell(header: true, text: "Registration number", html_attributes: {
scope: "col",
}) %>
<% row.cell(header: true, text: "Type", html_attributes: {
scope: "col",
}) %>
<% end %>
<% end %>
<% end %>
<% @organisations.each do |organisation| %>
<%= table.body do |body| %>
<%= body.row do |row| %>
<% row.cell(header: true, html_attributes: {
scope: "row",
}) do %>
<%= govuk_link_to(organisation.name, "organisations/#{organisation.id}/logs") %>
<% @organisations.each do |organisation| %>
<%= table.body do |body| %>
<%= body.row do |row| %>
<% row.cell(header: true, html_attributes: {
scope: "row",
}) do %>
<%= govuk_link_to(organisation.name, "organisations/#{organisation.id}/logs") %>
<% end %>
<% row.cell(text: organisation.housing_registration_no) %>
<% row.cell(text: organisation.display_provider_type) %>
<% end %>
<% row.cell(text: organisation.housing_registration_no) %>
<% row.cell(text: display_provider_type(organisation.provider_type)) %>
<% end %>
<% end %>
<% end %>
<% end %>
</section>

28
app/views/organisations/logs.html.erb

@ -1,9 +1,15 @@
<% content_for :title, "Logs" %>
<% item_label = @pagy.count > 1 ? "logs" : "log" %>
<% if @searched.present? %>
<% title = "Your organisation (#{@pagy.count} #{item_label} matching ‘#{@searched}’ of #{@total_count} total logs)" %>
<% else %>
<% title = "Your organisation (Logs)" %>
<% end %>
<h1 class="govuk-heading-l">
<span class="govuk-caption-l"><%= @organisation.name %></span>
<%= content_for(:title) %>
</h1>
<% content_for :title, title %>
<% content_for :tab_title do %>
<%= "Logs" %>
<% end %>
<%= render SubNavigationComponent.new(
items: secondary_items(request.path, @organisation.id),
@ -11,10 +17,10 @@
<div class="app-filter-layout" data-controller="filter-layout">
<%= render partial: "case_logs/log_filters" %>
<% if @case_logs.present? %>
<div class="app-filter-layout__content">
<%= render partial: "case_logs/log_list", locals: { case_logs: @case_logs, title: "Logs", pagy: @pagy } %>
<%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "logs" } %>
</div>
<% end %>
<div class="app-filter-layout__content">
<%= render SearchComponent.new(current_user:, search_label: "Search by log ID, tenant code, property reference or postcode", value: @searched) %>
<hr class="govuk-section-break govuk-section-break--visible govuk-section-break--m">
<%= render partial: "case_logs/log_list", locals: { case_logs: @case_logs, title: "Logs", pagy: @pagy, searched: @searched, item_label:, total_count: @total_count } %>
<%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "logs" } %>
</div>
</div>

2
app/views/organisations/show.html.erb

@ -1,4 +1,4 @@
<% content_for :title, "Your organisation (Details)" %>
<% content_for :title, current_user.support? ? "Details" : "Your organisation" %>
<% content_for :tab_title do %>
<%= "Details" %>

96
app/views/users/_user_list.html.erb

@ -1,54 +1,56 @@
<%= govuk_table do |table| %>
<%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<span class="govuk-!-margin-right-4">
<% if searched.present? %>
<strong><%= pagy.count %></strong> <%= item_label %> found matching ‘<%= searched %>’ of <strong><%= total_count %></strong> total users. <%= govuk_link_to("Clear search", request.path) %>
<% else %>
<strong><%= pagy.count %></strong> total users.
<section class="app-table-group" tabindex="0" aria-labelledby="<%= title.dasherize %>">
<%= govuk_table do |table| %>
<%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %>
<span class="govuk-!-margin-right-4">
<% if searched.present? %>
<strong><%= pagy.count %></strong> <%= item_label %> found matching ‘<%= searched %>’ of <strong><%= total_count %></strong> total users. <%= govuk_link_to("Clear search", request.path) %>
<% else %>
<strong><%= pagy.count %></strong> total users.
<% end %>
</span>
<% if current_user.support? %>
<% query = searched.present? ? "?search=#{searched}" : nil %>
<%= govuk_link_to "Download (CSV)", "/users.csv#{query}", type: "text/csv" %>
<% end %>
</span>
<% if current_user.support? %>
<% query = searched.present? ? "?search=#{searched}" : nil %>
<%= govuk_link_to "Download (CSV)", "/users.csv#{query}", type: "text/csv" %>
<% end %>
<% end %>
<%= table.head do |head| %>
<%= head.row do |row| %>
<% row.cell(header: true, text: "Name and email adress", html_attributes: {
scope: "col",
}) %>
<% row.cell(header: true, text: "Organisation and role", html_attributes: {
scope: "col",
}) %>
<% row.cell(header: true, text: "Last logged in", html_attributes: {
scope: "col",
}) %>
<%= table.head do |head| %>
<%= head.row do |row| %>
<% row.cell(header: true, text: "Name and email adress", html_attributes: {
scope: "col",
}) %>
<% row.cell(header: true, text: "Organisation and role", html_attributes: {
scope: "col",
}) %>
<% row.cell(header: true, text: "Last logged in", html_attributes: {
scope: "col",
}) %>
<% end %>
<% end %>
<% end %>
<% users.each do |user| %>
<%= table.body do |body| %>
<%= body.row do |row| %>
<% row.cell(header: true, html_attributes: {
scope: "row",
}) do %>
<%= simple_format(user_cell(user), {}, wrapper_tag: "span") %>
<% if user.is_data_protection_officer? || user.is_key_contact? %>
<br>
<% users.each do |user| %>
<%= table.body do |body| %>
<%= body.row do |row| %>
<% row.cell(header: true, html_attributes: {
scope: "row",
}) do %>
<%= simple_format(user_cell(user), {}, wrapper_tag: "span") %>
<% if user.is_data_protection_officer? || user.is_key_contact? %>
<br>
<% end %>
<%= user.is_data_protection_officer? ? govuk_tag(
classes: "app-tag--small",
colour: "turquoise",
text: "Data protection officer",
) : "" %>
<%= user.is_key_contact? ? govuk_tag(
classes: "app-tag--small",
colour: "turquoise",
text: "Key contact",
) : "" %>
<% end %>
<% row.cell(text: simple_format(org_cell(user), {}, wrapper_tag: "div")) %>
<% row.cell(text: user.active? ? user.last_sign_in_at&.to_formatted_s(:govuk_date) : "Deactivated") %>
<% end %>
<%= user.is_data_protection_officer? ? govuk_tag(
classes: "app-tag--small",
colour: "turquoise",
text: "Data protection officer",
) : "" %>
<%= user.is_key_contact? ? govuk_tag(
classes: "app-tag--small",
colour: "turquoise",
text: "Key contact",
) : "" %>
<% end %>
<% row.cell(text: simple_format(org_cell(user), {}, wrapper_tag: "div")) %>
<% row.cell(text: user.active? ? user.last_sign_in_at&.to_formatted_s(:govuk_date) : "Deactivated") %>
<% end %>
<% end %>
<% end %>
<% end %>
</section>

1
config/environments/test.rb

@ -60,4 +60,5 @@ Rails.application.configure do
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
Faker::Config.locale = "en-GB"
end

339
config/initializers/active_admin.rb

@ -1,339 +0,0 @@
ActiveAdmin.setup do |config|
# == Site Title
#
# Set the title that is displayed on the main layout
# for each of the active admin pages.
#
config.site_title = "DLUHC CORE"
# Set the link url for the title. For example, to take
# users to your main site. Defaults to no link.
#
# config.site_title_link = "/"
# Set an optional image to be displayed for the header
# instead of a string (overrides :site_title)
#
# Note: Aim for an image that's 21px high so it fits in the header.
#
# config.site_title_image = "logo.png"
# == Default Namespace
#
# Set the default namespace each administration resource
# will be added to.
#
# eg:
# config.default_namespace = :hello_world
#
# This will create resources in the HelloWorld module and
# will namespace routes to /hello_world/*
#
# To set no namespace by default, use:
# config.default_namespace = false
#
# Default:
# config.default_namespace = :admin
#
# You can customize the settings for each namespace by using
# a namespace block. For example, to change the site title
# within a namespace:
#
# config.namespace :admin do |admin|
# admin.site_title = "Custom Admin Title"
# end
#
# This will ONLY change the title for the admin section. Other
# namespaces will continue to use the main "site_title" configuration.
# == User Authentication
#
# Active Admin will automatically call an authentication
# method in a before filter of all controller actions to
# ensure that there is a currently logged in admin user.
#
# This setting changes the method which Active Admin calls
# within the application controller.
config.authentication_method = :authenticate_admin_user!
# == User Authorization
#
# Active Admin will automatically call an authorization
# method in a before filter of all controller actions to
# ensure that there is a user with proper rights. You can use
# CanCanAdapter or make your own. Please refer to documentation.
# config.authorization_adapter = ActiveAdmin::CanCanAdapter
# In case you prefer Pundit over other solutions you can here pass
# the name of default policy class. This policy will be used in every
# case when Pundit is unable to find suitable policy.
# config.pundit_default_policy = "MyDefaultPunditPolicy"
# If you wish to maintain a separate set of Pundit policies for admin
# resources, you may set a namespace here that Pundit will search
# within when looking for a resource's policy.
# config.pundit_policy_namespace = :admin
# You can customize your CanCan Ability class name here.
# config.cancan_ability_class = "Ability"
# You can specify a method to be called on unauthorized access.
# This is necessary in order to prevent a redirect loop which happens
# because, by default, user gets redirected to Dashboard. If user
# doesn't have access to Dashboard, he'll end up in a redirect loop.
# Method provided here should be defined in application_controller.rb.
# config.on_unauthorized_access = :access_denied
# == Current User
#
# Active Admin will associate actions with the current
# user performing them.
#
# This setting changes the method which Active Admin calls
# (within the application controller) to return the currently logged in user.
config.current_user_method = :current_admin_user
# == Logging Out
#
# Active Admin displays a logout link on each screen. These
# settings configure the location and method used for the link.
#
# This setting changes the path where the link points to. If it's
# a string, the strings is used as the path. If it's a Symbol, we
# will call the method to return the path.
#
# Default:
config.logout_link_path = :destroy_admin_user_session_path
# This setting changes the http method used when rendering the
# link. For example :get, :delete, :put, etc..
#
# Default:
# config.logout_link_method = :get
# == Root
#
# Set the action to call for the root path. You can set different
# roots for each namespace.
#
# Default:
# config.root_to = 'dashboard#index'
# == Admin Comments
#
# This allows your users to comment on any resource registered with Active Admin.
#
# You can completely disable comments:
config.comments = false
#
# You can change the name under which comments are registered:
# config.comments_registration_name = 'AdminComment'
#
# You can change the order for the comments and you can change the column
# to be used for ordering:
# config.comments_order = 'created_at ASC'
#
# You can disable the menu item for the comments index page:
# config.comments_menu = false
#
# You can customize the comment menu:
# config.comments_menu = { parent: 'Admin', priority: 1 }
# == Batch Actions
#
# Enable and disable Batch Actions
#
config.batch_actions = true
# == Controller Filters
#
# You can add before, after and around filters to all of your
# Active Admin resources and pages from here.
#
# config.before_action :do_something_awesome
# == Attribute Filters
#
# You can exclude possibly sensitive model attributes from being displayed,
# added to forms, or exported by default by ActiveAdmin
#
config.filter_attributes = %i[encrypted_password password password_confirmation]
# == Localize Date/Time Format
#
# Set the localize format to display dates and times.
# To understand how to localize your app with I18n, read more at
# https://guides.rubyonrails.org/i18n.html
#
# You can run `bin/rails runner 'puts I18n.t("date.formats")'` to see the
# available formats in your application.
#
config.localize_format = :long
# == Setting a Favicon
#
# config.favicon = 'favicon.ico'
# == Meta Tags
#
# Add additional meta tags to the head element of active admin pages.
#
# Add tags to all pages logged in users see:
# config.meta_tags = { author: 'My Company' }
# By default, sign up/sign in/recover password pages are excluded
# from showing up in search engine results by adding a robots meta
# tag. You can reset the hash of meta tags included in logged out
# pages:
# config.meta_tags_for_logged_out_pages = {}
# == Removing Breadcrumbs
#
# Breadcrumbs are enabled by default. You can customize them for individual
# resources or you can disable them globally from here.
#
# config.breadcrumb = false
# == Create Another Checkbox
#
# Create another checkbox is disabled by default. You can customize it for individual
# resources or you can enable them globally from here.
#
# config.create_another = true
# == Register Stylesheets & Javascripts
#
# We recommend using the built in Active Admin layout and loading
# up your own stylesheets / javascripts to customize the look
# and feel.
#
# To load a stylesheet:
# config.register_stylesheet 'my_stylesheet.css'
#
# You can provide an options hash for more control, which is passed along to stylesheet_link_tag():
# config.register_stylesheet 'my_print_stylesheet.css', media: :print
#
# To load a javascript file:
# config.register_javascript 'my_javascript.js'
# == CSV options
#
# Set the CSV builder separator
# config.csv_options = { col_sep: ';' }
#
# Force the use of quotes
# config.csv_options = { force_quotes: true }
# == Menu System
#
# You can add a navigation menu to be used in your application, or configure a provided menu
#
# To change the default utility navigation to show a link to your website & a logout btn
#
# config.namespace :admin do |admin|
# admin.build_menu :utility_navigation do |menu|
# menu.add label: "My Great Website", url: "http://www.mygreatwebsite.com", html_options: { target: :blank }
# admin.add_logout_button_to_menu menu
# end
# end
#
# If you wanted to add a static menu item to the default menu provided:
#
# config.namespace :admin do |admin|
# admin.build_menu :default do |menu|
# menu.add label: "My Great Website", url: "http://www.mygreatwebsite.com", html_options: { target: :blank }
# end
# end
# == Download Links
#
# You can disable download links on resource listing pages,
# or customize the formats shown per namespace/globally
#
# To disable/customize for the :admin namespace:
#
# config.namespace :admin do |admin|
#
# # Disable the links entirely
# admin.download_links = false
#
# # Only show XML & PDF options
# admin.download_links = [:xml, :pdf]
#
# # Enable/disable the links based on block
# # (for example, with cancan)
# admin.download_links = proc { can?(:view_download_links) }
#
# end
# == Pagination
#
# Pagination is enabled by default for all resources.
# You can control the default per page count for all resources here.
#
# config.default_per_page = 30
#
# You can control the max per page count too.
#
# config.max_per_page = 10_000
# == Filters
#
# By default the index screen includes a "Filters" sidebar on the right
# hand side with a filter for each attribute of the registered model.
# You can enable or disable them for all resources here.
#
# config.filters = true
#
# By default the filters include associations in a select, which means
# that every record will be loaded for each association (up
# to the value of config.maximum_association_filter_arity).
# You can enabled or disable the inclusion
# of those filters by default here.
#
# config.include_default_association_filters = true
# config.maximum_association_filter_arity = 256 # default value of :unlimited will change to 256 in a future version
# config.filter_columns_for_large_association = [
# :display_name,
# :full_name,
# :name,
# :username,
# :login,
# :title,
# :email,
# ]
# config.filter_method_for_large_association = '_starts_with'
# == Head
#
# You can add your own content to the site head like analytics. Make sure
# you only pass content you trust.
#
# config.head = ''.html_safe
# == Footer
#
# By default, the footer shows the current Active Admin version. You can
# override the content of the footer here.
#
# config.footer = 'my custom footer text'
# == Sorting
#
# By default ActiveAdmin::OrderClause is used for sorting logic
# You can inherit it with own class and inject it for all resources
#
# config.order_clause = MyOrderClause
# == Webpacker
#
# By default, Active Admin uses Sprocket's asset pipeline.
# You can switch to using Webpacker here.
#
# config.use_webpacker = true
end
Rails.application.config.after_initialize do
ActiveAdmin::BaseController.include Admin::PaperTrail
end

2
config/initializers/devise.rb

@ -152,7 +152,7 @@ Devise.setup do |config|
# their account can't be confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take
# before confirming their account.
config.confirm_within = 3.days
config.confirm_within = 5.days
# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email

25
config/routes.rb

@ -1,28 +1,5 @@
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
Rails.application.routes.draw do
devise_for :admin_users, {
path: :admin,
controllers: {
sessions: "auth/sessions",
passwords: "auth/passwords",
unlocks: "active_admin/devise/unlocks",
registrations: "active_admin/devise/registrations",
confirmations: "active_admin/devise/confirmations",
two_factor_authentication: "auth/two_factor_authentication",
},
path_names: {
sign_in: "sign-in",
sign_out: "sign-out",
two_factor_authentication: "two-factor-authentication",
two_factor_authentication_resend_code: "resend-code",
},
sign_out_via: %i[get],
}
devise_scope :admin_user do
get "admin/two-factor-authentication/resend", to: "auth/two_factor_authentication#show_resend", as: "admin_user_two_factor_authentication_resend"
end
devise_for :users, {
path: :account,
controllers: {
@ -48,8 +25,6 @@ Rails.application.routes.draw do
get "/health", to: ->(_) { [204, {}, [nil]] }
ActiveAdmin.routes(self)
root to: "start#index"
get "/accessibility-statement", to: "content#accessibility_statement"

36
db/migrate/20220525130518_drop_admin_users.rb

@ -0,0 +1,36 @@
class DropAdminUsers < ActiveRecord::Migration[7.0]
def up
drop_table :admin_users
end
def down
create_table "admin_users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at", precision: nil
t.datetime "remember_created_at", precision: nil
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "second_factor_attempts_count", default: 0
t.string "encrypted_otp_secret_key"
t.string "encrypted_otp_secret_key_iv"
t.string "encrypted_otp_secret_key_salt"
t.string "direct_otp"
t.datetime "direct_otp_sent_at", precision: nil
t.datetime "totp_timestamp", precision: nil
t.string "phone"
t.string "name"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at", precision: nil
t.datetime "last_sign_in_at", precision: nil
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.integer "failed_attempts", default: 0
t.string "unlock_token"
t.datetime "locked_at", precision: nil
t.index %w[encrypted_otp_secret_key], name: "index_admin_users_on_encrypted_otp_secret_key", unique: true
t.index %w[unlock_token], name: "index_admin_users_on_unlock_token", unique: true
end
end
end

31
db/schema.rb

@ -10,39 +10,10 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2022_05_23_150557) do
ActiveRecord::Schema[7.0].define(version: 2022_05_25_130518) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "admin_users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at", precision: nil
t.datetime "remember_created_at", precision: nil
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "second_factor_attempts_count", default: 0
t.string "encrypted_otp_secret_key"
t.string "encrypted_otp_secret_key_iv"
t.string "encrypted_otp_secret_key_salt"
t.string "direct_otp"
t.datetime "direct_otp_sent_at", precision: nil
t.datetime "totp_timestamp", precision: nil
t.string "phone"
t.string "name"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at", precision: nil
t.datetime "last_sign_in_at", precision: nil
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.integer "failed_attempts", default: 0
t.string "unlock_token"
t.datetime "locked_at", precision: nil
t.index ["encrypted_otp_secret_key"], name: "index_admin_users_on_encrypted_otp_secret_key", unique: true
t.index ["unlock_token"], name: "index_admin_users_on_unlock_token", unique: true
end
create_table "case_logs", force: :cascade do |t|
t.integer "status", default: 0
t.datetime "created_at", null: false

18
lib/tasks/data_import_field.rake

@ -0,0 +1,18 @@
namespace :core do
desc "Update database field from data XMLs provided by Softwire"
task :data_import_field, %i[field path] => :environment do |_task, args|
field = args[:field]
path = args[:path]
raise "Usage: rake core:data_import_field['field','path/to/xml_files']" if path.blank? || field.blank?
storage_service = StorageService.new(PaasConfigurationService.new, ENV["IMPORT_PAAS_INSTANCE"])
# We only allow a reduced list of known fields to be updatable
case field
when "tenant_code"
Imports::CaseLogsFieldImportService.new(storage_service).update_field(field, path)
else
raise "Field #{field} cannot be updated by data_import_field"
end
end
end

1
package.json

@ -5,7 +5,6 @@
"node": "^16.0.0"
},
"dependencies": {
"@activeadmin/activeadmin": "^2.11.0",
"@babel/core": "^7.17.7",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",

78
spec/controllers/admin/admin_users_controller_spec.rb

@ -1,78 +0,0 @@
require "rails_helper"
require_relative "../../support/devise"
describe Admin::AdminUsersController, type: :controller do
render_views
let(:page) { Capybara::Node::Simple.new(response.body) }
let(:resource_title) { "Admin Users" }
let(:valid_session) { {} }
let(:signed_in_admin_user) { FactoryBot.create(:admin_user) }
before do
sign_in signed_in_admin_user
end
describe "Get admin users" do
before do
get :index, session: valid_session
end
it "returns a table of admin users" do
expect(page).to have_content(resource_title)
expect(page).to have_table("index_table_admin_users")
expect(page).to have_link(AdminUser.first.id.to_s)
end
end
describe "Create admin users" do
let(:params) { { admin_user: { email: "test2@example.com", password: "pAssword1", phone: "07566126368" } } }
it "creates a new admin user" do
expect { post :create, session: valid_session, params: }.to change(AdminUser, :count).by(1)
end
it "tracks who created the record" do
post(:create, session: valid_session, params:)
created_id = response.location.match(/[0-9]+/)[0]
whodunnit_actor = AdminUser.find_by(id: created_id).versions.last.actor
expect(whodunnit_actor).to be_a(AdminUser)
expect(whodunnit_actor.id).to eq(signed_in_admin_user.id)
end
end
describe "Update admin users" do
context "when viewing the form" do
before do
get :edit, session: valid_session, params: { id: AdminUser.first.id }
end
it "shows the correct fields" do
expect(page).to have_field("admin_user_email")
expect(page).to have_field("admin_user_password")
expect(page).to have_field("admin_user_password_confirmation")
end
end
context "when updating an admin user" do
let(:admin_user) { FactoryBot.create(:admin_user) }
let(:email) { "new_email@example.com" }
let(:params) { { id: admin_user.id, admin_user: { email: } } }
before do
patch :update, session: valid_session, params:
end
it "updates the user without needing to input a password" do
admin_user.reload
expect(admin_user.email).to eq(email)
end
it "tracks who updated the record" do
admin_user.reload
whodunnit_actor = admin_user.versions.last.actor
expect(whodunnit_actor).to be_a(AdminUser)
expect(whodunnit_actor.id).to eq(signed_in_admin_user.id)
end
end
end
end

92
spec/controllers/admin/case_logs_controller_spec.rb

@ -1,92 +0,0 @@
require "rails_helper"
require_relative "../../support/devise"
describe Admin::CaseLogsController, type: :controller do
before do
sign_in admin_user
end
render_views
let(:page) { Capybara::Node::Simple.new(response.body) }
let(:resource_title) { "Logs" }
let(:valid_session) { {} }
let(:admin_user) { FactoryBot.create(:admin_user) }
let(:user) { FactoryBot.create(:user) }
describe "Get case logs" do
let!(:case_log) { FactoryBot.create(:case_log, :in_progress) }
before do
get :index, session: valid_session
end
it "returns a table of case logs" do
expect(page).to have_content(resource_title)
expect(page).to have_table("index_table_case_logs")
expect(page).to have_link(case_log.id.to_s)
expect(page).to have_link(case_log.owning_organisation.name.to_s)
end
end
describe "Create case logs" do
let(:owning_organisation) { FactoryBot.create(:organisation) }
let(:managing_organisation) { owning_organisation }
let(:params) do
{
"case_log": {
"owning_organisation_id": owning_organisation.id,
"managing_organisation_id": managing_organisation.id,
"created_by_id": user.id,
},
}
end
it "creates a new case log" do
expect { post :create, session: valid_session, params: }.to change(CaseLog, :count).by(1)
end
it "tracks who created the record" do
post(:create, session: valid_session, params:)
created_id = response.location.match(/[0-9]+/)[0]
whodunnit_actor = CaseLog.find_by(id: created_id).versions.last.actor
expect(whodunnit_actor).to be_a(AdminUser)
expect(whodunnit_actor.id).to eq(admin_user.id)
end
end
describe "Update case log" do
let!(:case_log) { FactoryBot.create(:case_log, :in_progress) }
context "when viewing the edit form" do
before do
get :edit, session: valid_session, params: { id: case_log.id }
end
it "has the correct fields" do
expect(page).to have_field("case_log_age1")
expect(page).to have_field("case_log_tenant_code")
end
end
context "when updating the case_log" do
let(:tenant_code) { "New tenant code by Admin" }
let(:params) { { id: case_log.id, case_log: { tenant_code: } } }
before do
patch :update, session: valid_session, params:
end
it "updates the case log" do
case_log.reload
expect(case_log.tenant_code).to eq(tenant_code)
end
it "tracks who updated the record" do
case_log.reload
whodunnit_actor = case_log.versions.last.actor
expect(whodunnit_actor).to be_a(AdminUser)
expect(whodunnit_actor.id).to eq(admin_user.id)
end
end
end
end

44
spec/controllers/admin/dashboard_controller_spec.rb

@ -1,44 +0,0 @@
require "rails_helper"
require_relative "../../support/devise"
describe Admin::DashboardController, type: :controller do
before do
sign_in admin_user
end
render_views
let(:page) { Capybara::Node::Simple.new(response.body) }
let(:resource_title) { "Dashboard" }
let(:valid_session) { {} }
let(:admin_user) { FactoryBot.create(:admin_user) }
describe "Get case logs" do
before do
2.times { |_| FactoryBot.create(:case_log, :in_progress) }
FactoryBot.create(:case_log, :completed)
get :index, session: valid_session
end
it "returns a dashboard page" do
expect(page).to have_content(resource_title)
end
it "returns a panel of recent case logs" do
expect(page).to have_xpath("//div[contains(@class, 'panel') and contains(//h3, 'Recent logs')]")
end
it "returns a panel of in progress case logs" do
panel_xpath = "//div[@class='panel' and .//h3[contains(., 'Total logs in progress')]]"
panel_content_xpath = "#{panel_xpath}//div[@class='panel_contents' and .//p[contains(., 2)]]"
expect(page).to have_xpath(panel_xpath)
expect(page).to have_xpath(panel_content_xpath)
end
it "returns a panel of completed case logs" do
panel_xpath = "//div[@class='panel' and .//h3[contains(., 'Total logs completed')]]"
panel_content_xpath = "#{panel_xpath}//div[@class='panel_contents' and .//p[contains(., 1)]]"
expect(page).to have_xpath(panel_xpath)
expect(page).to have_xpath(panel_content_xpath)
end
end
end

78
spec/controllers/admin/organisations_controller_spec.rb

@ -1,78 +0,0 @@
require "rails_helper"
require_relative "../../support/devise"
describe Admin::OrganisationsController, type: :controller do
render_views
let(:page) { Capybara::Node::Simple.new(response.body) }
let(:resource_title) { "Organisations" }
let(:valid_session) { {} }
let!(:organisation) { FactoryBot.create(:organisation) }
let!(:admin_user) { FactoryBot.create(:admin_user) }
before do
sign_in admin_user
end
describe "Organisations" do
before do
get :index, session: valid_session
end
it "returns a table of admin users" do
expect(page).to have_content(resource_title)
expect(page).to have_table("index_table_organisations")
expect(page).to have_link(organisation.id.to_s)
end
end
describe "Create organisation" do
let(:params) { { organisation: { name: "DLUHC", provider_type: "LA" } } }
it "creates a organisation" do
expect { post :create, session: valid_session, params: }.to change(Organisation, :count).by(1)
end
it "tracks who created the record" do
post(:create, session: valid_session, params:)
created_id = response.location.match(/[0-9]+/)[0]
whodunnit_actor = Organisation.find_by(id: created_id).versions.last.actor
expect(whodunnit_actor).to be_a(AdminUser)
expect(whodunnit_actor.id).to eq(admin_user.id)
end
end
describe "Update organisation" do
context "when viewing the edit form" do
before do
get :edit, session: valid_session, params: { id: organisation.id }
end
it "has the correct fields" do
expect(page).to have_field("organisation_name")
expect(page).to have_field("organisation_provider_type")
expect(page).to have_field("organisation_phone")
end
end
context "when updating the organisation" do
let(:name) { "New Org Name by Admin" }
let(:params) { { id: organisation.id, organisation: { name: } } }
before do
patch :update, session: valid_session, params:
end
it "updates the organisation" do
organisation.reload
expect(organisation.name).to eq(name)
end
it "tracks who updated the record" do
organisation.reload
whodunnit_actor = organisation.versions.last.actor
expect(whodunnit_actor).to be_a(AdminUser)
expect(whodunnit_actor.id).to eq(admin_user.id)
end
end
end
end

97
spec/controllers/admin/users_controller_spec.rb

@ -1,97 +0,0 @@
require "rails_helper"
require_relative "../../support/devise"
describe Admin::UsersController, type: :controller do
render_views
let!(:user) { FactoryBot.create(:user) }
let(:organisation) { FactoryBot.create(:organisation) }
let(:page) { Capybara::Node::Simple.new(response.body) }
let(:resource_title) { "Users" }
let(:valid_session) { {} }
let!(:admin_user) { FactoryBot.create(:admin_user) }
let(:notify_client) { instance_double(Notifications::Client) }
let(:devise_notify_mailer) { DeviseNotifyMailer.new }
before do
allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer)
allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client)
allow(notify_client).to receive(:send_email).and_return(true)
sign_in admin_user
end
describe "Get users" do
before do
get :index, session: valid_session
end
it "returns a table of users" do
expect(page).to have_content(resource_title)
expect(page).to have_table("index_table_users")
expect(page).to have_link(user.id.to_s)
end
end
describe "Create users" do
let(:params) do
{
user: {
email: "somethin5@example.com",
name: "Jane",
password: "pAssword1",
organisation_id: organisation.id,
role: "data_coordinator",
},
}
end
it "creates a new user" do
expect { post :create, session: valid_session, params: }.to change(User, :count).by(1)
end
it "tracks who created the record" do
post(:create, session: valid_session, params:)
created_id = response.location.match(/[0-9]+/)[0]
whodunnit_actor = User.find_by(id: created_id).versions.last.actor
expect(whodunnit_actor).to be_a(AdminUser)
expect(whodunnit_actor.id).to eq(admin_user.id)
end
end
describe "Update users" do
context "when viewing the edit form" do
before do
get :edit, session: valid_session, params: { id: user.id }
end
it "has the correct fields" do
expect(page).to have_field("user_email")
expect(page).to have_field("user_name")
expect(page).to have_field("user_organisation_id")
expect(page).to have_field("user_role")
expect(page).to have_field("user_password")
expect(page).to have_field("user_password_confirmation")
end
end
context "when updating the user" do
let(:name) { "Pete" }
let(:params) { { id: user.id, user: { name: } } }
before do
patch :update, session: valid_session, params:
end
it "updates the user without needing to input a password" do
user.reload
expect(user.name).to eq(name)
end
it "tracks who updated the record" do
user.reload
whodunnit_actor = user.versions.last.actor
expect(whodunnit_actor).to be_a(AdminUser)
expect(whodunnit_actor.id).to eq(admin_user.id)
end
end
end
end

18
spec/factories/case_log.rb

@ -11,9 +11,9 @@ FactoryBot.define do
end
trait :in_progress do
status { 1 }
tenant_code { "TH356" }
postcode_full { "PO5 3TE" }
ppostcode_full { "SW2 6HI" }
tenant_code { Faker::Name.initials(number: 10) }
postcode_full { Faker::Address.postcode }
ppostcode_full { Faker::Address.postcode }
age1 { 17 }
age2 { 19 }
end
@ -24,7 +24,7 @@ FactoryBot.define do
incfreq { 1 }
end
trait :conditional_section_complete do
tenant_code { "TH356" }
tenant_code { Faker::Name.initials(number: 10) }
age1 { 34 }
sex1 { "M" }
ethnic { 2 }
@ -34,7 +34,7 @@ FactoryBot.define do
end
trait :completed do
status { 2 }
tenant_code { "BZ737" }
tenant_code { Faker::Name.initials(number: 10) }
age1 { 35 }
sex1 { "F" }
ethnic { 2 }
@ -52,11 +52,11 @@ FactoryBot.define do
reservist { 0 }
illness { 1 }
preg_occ { 2 }
tenancy_code { "BZ757" }
tenancy_code { Faker::Name.initials(number: 10) }
startertenancy { 0 }
tenancylength { 5 }
tenancy { 1 }
ppostcode_full { "SE2 6RT" }
ppostcode_full { Faker::Address.postcode }
rsnvac { 6 }
unittype_gn { 7 }
beds { 3 }
@ -74,7 +74,7 @@ FactoryBot.define do
tcharge { 325 }
layear { 2 }
waityear { 1 }
postcode_full { "NW1 5TY" }
postcode_full { Faker::Address.postcode }
reasonpref { 1 }
cbl { 1 }
chr { 1 }
@ -112,7 +112,7 @@ FactoryBot.define do
needstype { 1 }
purchaser_code { 798_794 }
reason { 4 }
propcode { "123" }
propcode { Faker::Name.initials(number: 10) }
majorrepairs { 1 }
la { "E09000003" }
prevloc { "E07000105" }

162
spec/features/admin_panel_spec.rb

@ -1,162 +0,0 @@
require "rails_helper"
RSpec.describe "Admin Panel" do
let!(:admin) { FactoryBot.create(:admin_user) }
let(:devise_notify_mailer) { DeviseNotifyMailer.new }
let(:notify_client) { instance_double(Notifications::Client) }
let(:mfa_template_id) { AdminUser::MFA_TEMPLATE_ID }
let(:otp) { "999111" }
before do
allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer)
allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client)
allow(notify_client).to receive(:send_email).and_return(true)
end
it "shows the admin sign in page" do
visit("/admin")
expect(page).to have_current_path("/admin/sign-in")
expect(page).to have_content("Sign in to your CORE administration account")
end
context "with a valid 2FA code" do
before do
allow(SecureRandom).to receive(:random_number).and_return(otp)
visit("/admin")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
end
it "authenticates successfully" do
expect(notify_client).to receive(:send_email).with(
{
email_address: admin.email,
template_id: mfa_template_id,
personalisation: { otp: },
},
)
click_button("Sign in")
fill_in("code", with: otp)
click_button("Submit")
expect(page).to have_content("Dashboard")
expect(page).to have_content(I18n.t("devise.two_factor_authentication.success"))
end
context "but it is more than 15 minutes old" do
it "does not authenticate successfully" do
click_button("Sign in")
admin.update!(direct_otp_sent_at: 16.minutes.ago)
fill_in("code", with: otp)
click_button("Submit")
expect(page).to have_content("Check your email")
expect(page).to have_http_status(:unprocessable_entity)
expect(page).to have_title("Error")
expect(page).to have_selector("#error-summary-title")
end
end
end
context "with an invalid 2FA code" do
it "does not authenticate successfully" do
visit("/admin")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
click_button("Sign in")
fill_in("code", with: otp)
click_button("Submit")
expect(page).to have_content("Check your email")
expect(page).to have_http_status(:unprocessable_entity)
expect(page).to have_title("Error")
expect(page).to have_selector("#error-summary-title")
end
end
context "when the 2FA code needs to be resent" do
before do
visit("/admin")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
click_button("Sign in")
end
it "displays the resend view" do
click_link("Not received an email?")
expect(page).to have_button("Resend security code")
end
it "send a new OTP code and redirects back to the 2FA view" do
click_link("Not received an email?")
expect { click_button("Resend security code") }.to(change { admin.reload.direct_otp })
expect(page).to have_current_path("/admin/two-factor-authentication")
end
end
context "when logging out and in again" do
before do
allow(SecureRandom).to receive(:random_number).and_return(otp)
end
it "requires the 2FA code on each login" do
visit("/admin")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
click_button("Sign in")
fill_in("code", with: otp)
click_button("Submit")
click_link("Logout")
visit("/admin")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
click_button("Sign in")
expect(page).to have_content("Check your email")
end
end
context "when the admin has forgotten their password" do
let!(:admin_user) { FactoryBot.create(:admin_user, last_sign_in_at: Time.zone.now) }
let(:reset_password_token) { "MCDH5y6Km-U7CFPgAMVS" }
before do
allow(Devise.token_generator).to receive(:generate).and_return(reset_password_token)
end
it " is redirected to the reset password page when they click the reset password link" do
visit("/admin")
click_link("reset your password")
expect(page).to have_current_path("/admin/password/new")
end
it " is shown an error message if they submit without entering an email address" do
visit("/admin/password/new")
click_button("Send email")
expect(page).to have_selector("#error-summary-title")
expect(page).to have_selector("#user-email-field-error")
expect(page).to have_title("Error")
end
it " is redirected to admin login page after reset email is sent" do
visit("/admin/password/new")
fill_in("admin_user[email]", with: admin_user.email)
click_button("Send email")
expect(page).to have_content("Check your email")
end
it " is sent a reset password email via Notify" do
expect(notify_client).to receive(:send_email).with(
{
email_address: admin_user.email,
template_id: admin_user.reset_password_notify_template,
personalisation: {
name: admin_user.email,
email: admin_user.email,
organisation: "",
link: "http://localhost:3000/admin/password/edit?reset_password_token=#{reset_password_token}",
},
},
)
visit("/admin/password/new")
fill_in("admin_user[email]", with: admin_user.email)
click_button("Send email")
end
end
end

50
spec/features/auth/user_lockout_spec.rb

@ -2,7 +2,6 @@ require "rails_helper"
RSpec.describe "User Lockout" do
let(:user) { FactoryBot.create(:user) }
let(:admin) { FactoryBot.create(:admin_user) }
let(:max_login_attempts) { Devise.maximum_attempts }
let(:max_2fa_attempts) { Devise.max_login_attempts }
let(:notify_client) { instance_double(Notifications::Client) }
@ -26,53 +25,4 @@ RSpec.describe "User Lockout" do
expect(page).to have_content(I18n.t("devise.failure.locked"))
end
end
context "when login-in with the wrong admin password up to a maximum number of attempts" do
before do
visit("/admin/sign-in")
max_login_attempts.times do
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: "wrong_password")
click_button("Sign in")
end
end
it "locks the admin account" do
visit("/admin/sign-in")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
click_button("Sign in")
expect(page).to have_http_status(:unprocessable_entity)
expect(page).to have_content(I18n.t("devise.failure.locked"))
end
end
context "when login-in with the right admin password and incorrect 2FA token up to a maximum number of attempts" do
let(:devise_notify_mailer) { DeviseNotifyMailer.new }
before do
allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer)
allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client)
allow(notify_client).to receive(:send_email).and_return(true)
visit("/admin/sign-in")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
click_button("Sign in")
max_2fa_attempts.times do
fill_in("code", with: "random")
click_button("Submit")
end
end
it "locks the admin account" do
visit("/admin/sign-in")
fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password)
click_button("Sign in")
expect(page).to have_http_status(:unprocessable_entity)
expect(page).to have_content(I18n.t("devise.two_factor_authentication.account_locked"))
end
end
end

2
spec/features/form/saving_data_spec.rb

@ -73,7 +73,7 @@ RSpec.describe "Form Saving Data" do
it "displays number answers in inputs if they are already saved" do
visit("/logs/#{id}/property-postcode")
expect(page).to have_field("case-log-postcode-full-field", with: "PO53TE")
expect(page).to have_field("case-log-postcode-full-field", with: case_log.postcode_full)
end
it "displays text answers in inputs if they are already saved" do

59
spec/features/log_spec.rb

@ -0,0 +1,59 @@
require "rails_helper"
RSpec.describe "Log Features" do
context "when searching for specific logs" do
context "when I am logged in and there are logs in the database" do
let(:user) { FactoryBot.create(:user, last_sign_in_at: Time.zone.now) }
let!(:log_to_search) { FactoryBot.create(:case_log, owning_organisation: user.organisation) }
let!(:same_organisation_log) { FactoryBot.create(:case_log, owning_organisation: user.organisation) }
let!(:another_organisation_log) { FactoryBot.create(:case_log) }
before do
visit("/logs")
fill_in("user[email]", with: user.email)
fill_in("user[password]", with: user.password)
click_button("Sign in")
end
it "displays the logs belonging to the same organisation" do
expect(page).to have_link(log_to_search.id.to_s)
expect(page).to have_link(same_organisation_log.id.to_s)
expect(page).not_to have_link(another_organisation_log.id.to_s)
end
context "when I search for a specific log" do
it "there is a search bar with a message and search button for logs" do
expect(page).to have_field("search")
expect(page).to have_content("Search by log ID, tenant code, property reference or postcode")
expect(page).to have_button("Search")
end
context "when I fill in search information and press the search button" do
before do
fill_in("search", with: log_to_search.id)
click_button("Search")
end
it "displays log matching the log ID" do
expect(page).to have_link(log_to_search.id.to_s)
expect(page).not_to have_link(same_organisation_log.id.to_s)
expect(page).not_to have_link(another_organisation_log.id.to_s)
end
context "when I want to clear results" do
it "there is link to clear the search results" do
expect(page).to have_link("Clear search")
end
it "displays the logs belonging to the same organisation after I clear the search results" do
click_link("Clear search")
expect(page).to have_link(log_to_search.id.to_s)
expect(page).to have_link(same_organisation_log.id.to_s)
expect(page).not_to have_link(another_organisation_log.id.to_s)
end
end
end
end
end
end
end

47
spec/features/organisation_spec.rb

@ -84,12 +84,13 @@ RSpec.describe "User Features" do
context "when user is support user" do
context "when viewing logs for specific organisation" do
let(:user) { FactoryBot.create(:user, :support) }
let(:number_of_case_logs) { 4 }
let(:first_log) { organisation.case_logs.first }
let(:otp) { "999111" }
let!(:log_to_search) { FactoryBot.create(:case_log, owning_organisation: user.organisation, managing_organisation_id: organisation.id) }
let!(:other_logs) { FactoryBot.create_list(:case_log, 4, owning_organisation_id: organisation.id, managing_organisation_id: organisation.id) }
let(:number_of_case_logs) { CaseLog.count }
before do
FactoryBot.create_list(:case_log, number_of_case_logs, owning_organisation_id: organisation.id, managing_organisation_id: organisation.id)
first_log.update!(startdate: Time.utc(2022, 6, 2, 10, 36, 49))
allow(SecureRandom).to receive(:random_number).and_return(otp)
click_link("Sign out")
@ -99,6 +100,48 @@ RSpec.describe "User Features" do
visit("/organisations/#{org_id}/logs")
end
context "when searching for specific logs" do
it "displays the logs belonging to the same organisation" do
expect(page).to have_content(log_to_search.id)
other_logs.each do |log|
expect(page).to have_content(log.id)
end
end
context "when I search for a specific log" do
it "there is a search bar with a message and search button for logs" do
expect(page).to have_field("search")
expect(page).to have_content("Search by log ID, tenant code, property reference or postcode")
expect(page).to have_button("Search")
end
context "when I fill in search information and press the search button" do
before do
fill_in("search", with: log_to_search.id)
click_button("Search")
end
it "displays log matching the log ID" do
expect(page).to have_link(log_to_search.id.to_s)
other_logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
context "when I want to clear results" do
it "there is link to clear the search results" do
expect(page).to have_link("Clear search")
end
it "displays the logs belonging to the same organisation after I clear the search result after I clear the search resultss" do
click_link("Clear search")
expect(page).to have_link(log_to_search.id.to_s)
end
end
end
end
end
it "can filter case logs" do
expect(page).to have_content("#{number_of_case_logs} total logs")
organisation.case_logs.map(&:id).each do |case_log_id|

2
spec/fixtures/exports/case_logs.csv vendored

@ -1,2 +1,2 @@
status,age1,sex1,ethnic,national,prevten,ecstat1,hhmemb,age2,sex2,ecstat2,age3,sex3,ecstat3,age4,sex4,ecstat4,age5,sex5,ecstat5,age6,sex6,ecstat6,age7,sex7,ecstat7,age8,sex8,ecstat8,homeless,underoccupation_benefitcap,leftreg,reservist,illness,preg_occ,startertenancy,tenancylength,tenancy,ppostcode_full,rsnvac,unittype_gn,beds,offered,wchair,earnings,incfreq,benefits,period,layear,waityear,postcode_full,reasonpref,cbl,chr,cap,reasonother,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_f,housingneeds_g,housingneeds_h,illness_type_1,illness_type_2,illness_type_3,illness_type_4,illness_type_8,illness_type_5,illness_type_6,illness_type_7,illness_type_9,illness_type_10,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,tenancyother,irproduct_other,reason,propcode,la,prevloc,hb,hbrentshortfall,mrcdate,incref,startdate,armedforces,unitletas,builtype,voiddate,renttype,needstype,lettype,totchild,totelder,totadult,nocharge,referral,brent,scharge,pscharge,supcharg,tcharge,tshortfall,chcharge,has_benefits,renewal,wrent,wscharge,wpschrge,wsupchrg,wtcharge,wtshortfall,refused,housingneeds,wchchrg,newprop,relat2,relat3,relat4,relat5,relat6,relat7,relat8,lar,irproduct,joint,shelteredaccom,form,owningorgid,owningorgname,hcnum,maningorgid,maningorgname,manhcnum,createddate,uploaddate,tenancycode,ppcodenk
2,35,F,2,4,6,0,2,32,M,6,,,,,,,,,,,,,,,,,,,1,0,1,0,1,2,0,5,1,SE26RT,6,7,3,2,1,68,1,1,2,2,1,NW15TY,1,1,1,0,,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,,,4,123,E09000003,E07000105,6,1,2020-05-05 10:36:49 UTC,0,2022-02-02 10:36:49 UTC,1,2,1,2019-11-03 00:00:00 UTC,2,1,7,0,0,2,0,,200.0,50.0,40.0,35.0,325.0,12.0,,1,0,100.0,25.0,20.0,17.5,162.5,6.0,0,1,,2,P,,,,,,,,,,0,{id},{owning_org_id},DLUHC,1234,{managing_org_id},DLUHC,1234,2022-02-08 16:52:15 UTC,2022-02-08 16:52:15 UTC,BZ757,1
2,35,F,2,4,6,0,2,32,M,6,,,,,,,,,,,,,,,,,,,1,0,1,0,1,2,0,5,1,SE26RT,6,7,3,2,1,68,1,1,2,2,1,NW15TY,1,1,1,0,,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,,,4,123,E09000003,E07000105,6,1,2020-05-05 10:36:49 UTC,0,2022-02-02 10:36:49 UTC,1,2,1,2019-11-03 00:00:00 UTC,2,1,7,0,0,2,0,,200.0,50.0,40.0,35.0,325.0,12.0,,1,0,100.0,25.0,20.0,17.5,162.5,6.0,0,1,,2,P,,,,,,,,,,0,{id},{owning_org_id},DLUHC,1234,{managing_org_id},DLUHC,1234,2022-02-08 16:52:15 UTC,2022-02-08 16:52:15 UTC,BZ737,1

1 status age1 sex1 ethnic national prevten ecstat1 hhmemb age2 sex2 ecstat2 age3 sex3 ecstat3 age4 sex4 ecstat4 age5 sex5 ecstat5 age6 sex6 ecstat6 age7 sex7 ecstat7 age8 sex8 ecstat8 homeless underoccupation_benefitcap leftreg reservist illness preg_occ startertenancy tenancylength tenancy ppostcode_full rsnvac unittype_gn beds offered wchair earnings incfreq benefits period layear waityear postcode_full reasonpref cbl chr cap reasonother housingneeds_a housingneeds_b housingneeds_c housingneeds_f housingneeds_g housingneeds_h illness_type_1 illness_type_2 illness_type_3 illness_type_4 illness_type_8 illness_type_5 illness_type_6 illness_type_7 illness_type_9 illness_type_10 rp_homeless rp_insan_unsat rp_medwel rp_hardship rp_dontknow tenancyother irproduct_other reason propcode la prevloc hb hbrentshortfall mrcdate incref startdate armedforces unitletas builtype voiddate renttype needstype lettype totchild totelder totadult nocharge referral brent scharge pscharge supcharg tcharge tshortfall chcharge has_benefits renewal wrent wscharge wpschrge wsupchrg wtcharge wtshortfall refused housingneeds wchchrg newprop relat2 relat3 relat4 relat5 relat6 relat7 relat8 lar irproduct joint shelteredaccom form owningorgid owningorgname hcnum maningorgid maningorgname manhcnum createddate uploaddate tenancycode ppcodenk
2 2 35 F 2 4 6 0 2 32 M 6 1 0 1 0 1 2 0 5 1 SE26RT 6 7 3 2 1 68 1 1 2 2 1 NW15TY 1 1 1 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 4 123 E09000003 E07000105 6 1 2020-05-05 10:36:49 UTC 0 2022-02-02 10:36:49 UTC 1 2 1 2019-11-03 00:00:00 UTC 2 1 7 0 0 2 0 200.0 50.0 40.0 35.0 325.0 12.0 1 0 100.0 25.0 20.0 17.5 162.5 6.0 0 1 2 P 0 {id} {owning_org_id} DLUHC 1234 {managing_org_id} DLUHC 1234 2022-02-08 16:52:15 UTC 2022-02-08 16:52:15 UTC BZ757 BZ737 1

2
spec/fixtures/exports/case_logs.xml vendored

@ -139,7 +139,7 @@
<manhcnum>1234</manhcnum>
<createddate>2022-02-08 16:52:15 UTC</createddate>
<uploaddate>2022-02-08 16:52:15 UTC</uploaddate>
<tenancycode>BZ757</tenancycode>
<tenancycode>BZ737</tenancycode>
<ppcodenk>1</ppcodenk>
<providertype>1</providertype>
</form>

514
spec/fixtures/softwire_imports/case_logs/5ybz29dj-l33t-k1l0-hj86-n4k4ma77xkcd.xml vendored

@ -0,0 +1,514 @@
<Group xmlns="http://data.gov.uk/core/logs/2021-CORE-IR-GN" xmlns:app="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:meta="http://data.gov.uk/core/metadata" xmlns:svc="http://www.w3.org/2007/app" xmlns:xf="http://www.w3.org/2002/xforms" xmlns:xfimpl="http://www.w3.org/2002/xforms/implementation" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xxf="http://orbeon.org/oxf/xml/xforms" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<meta:metadata xmlns:es="http://www.ecmascript.org/" xmlns:xqx="http://www.w3.org/2005/XQueryX" xmlns:XSLT="http://www.w3.org/1999/XSL/Transform/compile">
<meta:form-name>2021-CORE-IR-GN</meta:form-name>
<meta:document-id>5ybz29dj-l33t-k1l0-hj86-n4k4ma77xkcd</meta:document-id>
<meta:owner-user-id>e29c492473446dca4d50224f2bb7cf965a261d6f</meta:owner-user-id>
<meta:owner-institution-id>7c5bd5fb549c09a2c55d7cb90d7ba84927e64618</meta:owner-institution-id>
<meta:managing-institution-id>7c5bd5fb549c09a2c55d7cb90d7ba84927e64618</meta:managing-institution-id>
<meta:created-date>2022-04-11T13:46:23.953121Z</meta:created-date>
<meta:modified-date>2022-04-11T13:46:23.953121Z</meta:modified-date>
<meta:status>submitted-valid</meta:status>
<meta:reporting-year>2021</meta:reporting-year>
<meta:upload-method>Manual Entry</meta:upload-method>
<meta:schema assert-valid="true"/>
<meta:rules assert-valid="true"/>
</meta:metadata>
<Group>
<Qdp>Yes</Qdp>
<IRProduct>2 London Living Rent</IRProduct>
<IRProductOther/>
<KeyDate>2021-11-10</KeyDate>
<FORM>300072</FORM>
<Landlord source-value="9">1 Private Registered Provider</Landlord>
<Group>
<_1btenagree>1 This Landlord</_1btenagree>
<_1bifanother/>
<_1bCOREcode/>
</Group>
</Group>
<Group>
<_2a>2 No</_2a>
<Q2b>2 Assured</Q2b>
<Q2ba/>
<_2bTenCode>TEN0021</_2bTenCode>
<_2cYears/>
</Group>
<Group>
<P1Age override-field="">37</P1Age>
<P1AR/>
<P1Sex override-field="">Female</P1Sex>
<P1Eco>1) Full Time</P1Eco>
<P1Eth>1 White: English/Scottish/Welsh/Northern Irish/British</P1Eth>
<P1Nat>1 UK national resident in UK</P1Nat>
<P2Age override-field="">34</P2Age>
<P2AR/>
<P2Sex override-field="">Female</P2Sex>
<P2Rel>Other</P2Rel>
<P2Eco>1) Full Time</P2Eco>
<P3Age override-field=""/>
<P3AR/>
<P3Sex override-field=""/>
<P3Rel/>
<P3Eco/>
<P4Age override-field=""/>
<P4AR/>
<P4Sex override-field=""/>
<P4Rel/>
<P4Eco/>
<P5Age override-field=""/>
<P5AR/>
<P5Sex override-field=""/>
<P5Rel/>
<P5Eco/>
<P6Age override-field=""/>
<P6AR/>
<P6Sex override-field=""/>
<P6Rel/>
<P6Eco/>
<P7Age override-field=""/>
<P7AR/>
<P7Sex override-field=""/>
<P7Rel/>
<P7Eco/>
<P8Age override-field=""/>
<P8AR/>
<P8Sex override-field=""/>
<P8Rel/>
<P8Eco/>
<Group>
<ArmedF>2 No</ArmedF>
<LeftAF/>
<Inj/>
<Preg override-field="">2 No</Preg>
</Group>
<Group>
<Q6Ben>7 UC &#x2013; without housing element (not in receipt of HB and tenants not eligible for housing support e.g. residential care home)</Q6Ben>
</Group>
<Group>
<Q7Ben>3 None</Q7Ben>
<Q8Refused/>
<Q8Money override-field=""/>
<Q8a/>
</Group>
<Group>
<Q9a>8 (Non-violent) relationship breakdown with partner</Q9a>
<Q9aa/>
</Group>
<Group>
<_9b override-field="">2 No</_9b>
<Q10-a/>
<Q10-b/>
<Q10-c/>
<Q10-f/>
<Q10-g>Yes</Q10-g>
<Q10-h/>
<Q10ia>1 Yes</Q10ia>
<Q10ib-1/>
<Q10ib-2/>
<Q10ib-3/>
<Q10ib-4>Yes</Q10ib-4>
<Q10ib-5/>
<Q10ib-6/>
<Q10ib-7/>
<Q10ib-8/>
<Q10ib-9/>
<Q10ib-10/>
<Q11 override-field="">28 Living with friends or family</Q11>
<Q12a>Blackpool</Q12a>
<Q12aONS>E09000009</Q12aONS>
<Q12b override-field="">FY1 1JD</Q12b>
<Q12bnot/>
<Q12c>1 Just moved to Local Authority area</Q12c>
<Q12d>1 Just moved to Local Authority area</Q12d>
</Group>
<Group>
<Q13>1 Not homeless</Q13>
<Q14a>2 No</Q14a>
<Q14b1/>
<Q14b2/>
<Q14b3/>
<Q14b4/>
<Q14b5/>
</Group>
<Group>
<Q15CBL>2 No</Q15CBL>
<Q15CHR>2 No</Q15CHR>
<Q15CAP>2 No</Q15CAP>
</Group>
<Group>
<Q16>10 Other social landlord</Q16>
</Group>
</Group>
<Group>
<Q17>1 Weekly for 52 weeks</Q17>
<Q18ai override-field="">100.00</Q18ai>
<Q18aii override-field=""/>
<Q18aiii override-field=""/>
<Q18aiv override-field=""/>
<Q18av override-field="">100.00</Q18av>
<Q18d/>
<Q18dyes override-field=""/>
<Q19void>2021-04-15</Q19void>
<Q19repair/>
<Q20 override-field="">0</Q20>
<Q21a>PROPREF0042</Q21a>
</Group>
<Group>
<Q22 override-field=""/>
<Q23>1 Flat / maisonette</Q23>
<Q24>1 Purpose built</Q24>
<Q25>2 No</Q25>
<Q26>4 An Intermediate Rent basis</Q26>
<Q27>10 Relet - tenant evicted due to arrears</Q27>
</Group>
<Group>
<Q28Auth>Basingstoke &amp; Deane</Q28Auth>
<Q28ONS>E07000084</Q28ONS>
<Q28pc override-field="">RG21 3HU</Q28pc>
</Group>
<Group>
<F1Age>1</F1Age>
<F2Age>1</F2Age>
<F3Age>0</F3Age>
<F4Age>0</F4Age>
<F5Age>0</F5Age>
<F6Age>0</F6Age>
<F7Age>0</F7Age>
<F8Age>0</F8Age>
<FAge>2</FAge>
<F1>1</F1>
<F2>1</F2>
<F3>0</F3>
<F4>0</F4>
<F5>0</F5>
<F6>0</F6>
<F7>0</F7>
<F8>0</F8>
<F>2</F>
<P1100>0</P1100>
<P2100>0</P2100>
<P3100>0</P3100>
<P4100>0</P4100>
<P5100>0</P5100>
<P6100>0</P6100>
<P7100>0</P7100>
<P8100>0</P8100>
<_100>0</_100>
<P170>0</P170>
<P270>0</P270>
<P370>0</P370>
<P470>0</P470>
<P570>0</P570>
<P670>0</P670>
<P770>0</P770>
<P870>0</P870>
<_70>0</_70>
<P1PT>0</P1PT>
<P2PT>0</P2PT>
<P3PT>0</P3PT>
<P4PT>0</P4PT>
<P5PT>0</P5PT>
<P6PT>0</P6PT>
<P7PT>0</P7PT>
<P8PT>0</P8PT>
<PT>0</PT>
<P1FT>1</P1FT>
<P2FT>1</P2FT>
<P3FT>0</P3FT>
<P4FT>0</P4FT>
<P5FT>0</P5FT>
<P6FT>0</P6FT>
<P7FT>0</P7FT>
<P8FT>0</P8FT>
<FT>2</FT>
<P1Stud>0</P1Stud>
<P2Stud>0</P2Stud>
<P3Stud>0</P3Stud>
<P4Stud>0</P4Stud>
<P5Stud>0</P5Stud>
<P6Stud>0</P6Stud>
<P7Stud>0</P7Stud>
<P8Stud>0</P8Stud>
<Stud>0</Stud>
<P2Child>0</P2Child>
<P3Child>0</P3Child>
<P4Child>0</P4Child>
<P5Child>0</P5Child>
<P6Child>0</P6Child>
<P7Child>0</P7Child>
<P8Child>0</P8Child>
<Child>0</Child>
<P2Partner>0</P2Partner>
<P3Partner>0</P3Partner>
<P4Partner>0</P4Partner>
<P5Partner>0</P5Partner>
<P6Partner>0</P6Partner>
<P7Partner>0</P7Partner>
<P8Partner>0</P8Partner>
<Partner>0</Partner>
<Q1bV1>0</Q1bV1>
<Q1bV2>0</Q1bV2>
<Q1bV3>1</Q1bV3>
<Q1bVT>1</Q1bVT>
<P1Adult>1</P1Adult>
<P2Adult>1</P2Adult>
<P3Adult>0</P3Adult>
<P4Adult>0</P4Adult>
<P5Adult>0</P5Adult>
<P6Adult>0</P6Adult>
<P7Adult>0</P7Adult>
<P8Adult>0</P8Adult>
<PAdultT>2</PAdultT>
<P2PAge>0</P2PAge>
<P3PAge>0</P3PAge>
<P4PAge>0</P4PAge>
<P5PAge>0</P5PAge>
<P6PAge>0</P6PAge>
<P7PAge>0</P7PAge>
<P8PAge>0</P8PAge>
<PAGE>37</PAGE>
<P2ChildAge>0</P2ChildAge>
<P3ChildAge>0</P3ChildAge>
<P4ChildAge>0</P4ChildAge>
<P5ChildAge>0</P5ChildAge>
<P6ChildAge>0</P6ChildAge>
<P7ChildAge>0</P7ChildAge>
<P8ChildAge>0</P8ChildAge>
<ChildAgeMin>0</ChildAgeMin>
<AgeDiff1>37</AgeDiff1>
<AgeDiff2>0</AgeDiff2>
<AgeDiff3>37</AgeDiff3>
<TODAY>2022-04-12Z</TODAY>
<FutureLimit>2022-04-27Z</FutureLimit>
<minmax1/>
<minmax2/>
<minmax3/>
<minmax4/>
<minmax5/>
<minmax6/>
<minmax7/>
<minmax8/>
<minmax9/>
<minmax0/>
<minmax10/>
<minmaxT/>
<Q10av>0</Q10av>
<Q10bv>0</Q10bv>
<Q10cv>0</Q10cv>
<Q10fv>0</Q10fv>
<Q10gv>20</Q10gv>
<Q10hv>0</Q10hv>
<Q10Validate>20</Q10Validate>
<Q2bv>C</Q2bv>
<P2Agev>1</P2Agev>
<P2Sexv>1</P2Sexv>
<P2Relv>1</P2Relv>
<P2Ecov>1</P2Ecov>
<P2valid>4</P2valid>
<P3Agev>0</P3Agev>
<P3Sexv>0</P3Sexv>
<P3Relv>0</P3Relv>
<P3Ecov>0</P3Ecov>
<P3valid>0</P3valid>
<P4Agev>0</P4Agev>
<P4Sexv>0</P4Sexv>
<P4Relv>0</P4Relv>
<P4Ecov>0</P4Ecov>
<P4valid>0</P4valid>
<P5Agev>0</P5Agev>
<P5Sexv>0</P5Sexv>
<P5Relv>0</P5Relv>
<P5Ecov>0</P5Ecov>
<P5valid>0</P5valid>
<P6Agev>0</P6Agev>
<P6Sexv>0</P6Sexv>
<P6Relv>0</P6Relv>
<P6Ecov>0</P6Ecov>
<P6valid>0</P6valid>
<P7Agev>0</P7Agev>
<P7Sexv>0</P7Sexv>
<P7Relv>0</P7Relv>
<P7Ecov>0</P7Ecov>
<P7valid>0</P7valid>
<P8Agev>0</P8Agev>
<P8Sexv>0</P8Sexv>
<P8Relv>0</P8Relv>
<P8Ecov>0</P8Ecov>
<P8valid>0</P8valid>
<Q14b1v>0</Q14b1v>
<Q14b2v>0</Q14b2v>
<Q14b3v>0</Q14b3v>
<Q14b4v>0</Q14b4v>
<Q14b5v>0</Q14b5v>
<Q14bv>0</Q14bv>
<P2Other>1</P2Other>
<P3Other>0</P3Other>
<P4Other>0</P4Other>
<P5Other>0</P5Other>
<P6Other>0</P6Other>
<P7Other>0</P7Other>
<P8Other>0</P8Other>
<Other>1</Other>
<P2ARefused>0</P2ARefused>
<P3ARefused>0</P3ARefused>
<P4ARefused>0</P4ARefused>
<P5ARefused>0</P5ARefused>
<P6ARefused>0</P6ARefused>
<P7ARefused>0</P7ARefused>
<P8ARefused>0</P8ARefused>
<TAREUSED>0</TAREUSED>
<P2RRefused>0</P2RRefused>
<P3RRefused>0</P3RRefused>
<P4RRefused>0</P4RRefused>
<P5RRefused>0</P5RRefused>
<P6RRefused>0</P6RRefused>
<P7RRefused>0</P7RRefused>
<P8RRefused>0</P8RRefused>
<TotRRefused>0</TotRRefused>
<TOTREFUSED>0</TOTREFUSED>
</Group>
<Group>
<TOTADULT>2</TOTADULT>
<NEW_OLD>2 Existing Tenant</NEW_OLD>
<Q18aValid>1</Q18aValid>
<Q18Valid>1</Q18Valid>
<VACDAYS>209</VACDAYS>
<HHMEMB>2</HHMEMB>
<HHTYPEP1A>1</HHTYPEP1A>
<HHTYPEP2A>1</HHTYPEP2A>
<HHTYPEP3A>0</HHTYPEP3A>
<HHTYPEP4A>0</HHTYPEP4A>
<HHTYPEP5A>0</HHTYPEP5A>
<HHTYPEP6A>0</HHTYPEP6A>
<HHTYPEP7A>0</HHTYPEP7A>
<HHTYPEP8A>0</HHTYPEP8A>
<TADULT>2</TADULT>
<HHTYPEP1E>0</HHTYPEP1E>
<HHTYPEP2E>0</HHTYPEP2E>
<HHTYPEP3E>0</HHTYPEP3E>
<HHTYPEP4E>0</HHTYPEP4E>
<HHTYPEP5E>0</HHTYPEP5E>
<HHTYPEP6E>0</HHTYPEP6E>
<HHTYPEP7E>0</HHTYPEP7E>
<HHTYPEP8E>0</HHTYPEP8E>
<TELDER>0</TELDER>
<HHTYPEP1C>0</HHTYPEP1C>
<HHTYPEP2C>0</HHTYPEP2C>
<HHTYPEP3C>0</HHTYPEP3C>
<HHTYPEP4C>0</HHTYPEP4C>
<HHTYPEP5C>0</HHTYPEP5C>
<HHTYPEP6C>0</HHTYPEP6C>
<HHTYPEP7C>0</HHTYPEP7C>
<HHTYPEP8C>0</HHTYPEP8C>
<TCHILD>0</TCHILD>
<HHTYPE>4 = 2 adults</HHTYPE>
<WEEKLYINC>200.00</WEEKLYINC>
<INCOME>200.00</INCOME>
<TYPEHB>13</TYPEHB>
<AFFRATE/>
<Weekinc>200.00</Weekinc>
<LETTYPE>1 Private Registered Provider</LETTYPE>
<PLOACODE/>
<OACODE/>
<GOVREG>E12000008</GOVREG>
<OWNINGORGID>1</OWNINGORGID>
<OWNINGORGNAME>DLUHC</OWNINGORGNAME>
<MANINGORGNAME>DLUHC</MANINGORGNAME>
<HCNUM>655</HCNUM>
<MANHCNUM>655</MANHCNUM>
<LAHA/>
<MANINGORGID>1</MANINGORGID>
<HBTYPE1/>
<HBTYPE2/>
<HBTYPE3/>
<HBTYPE4/>
<HBTYPE5/>
<HBTYPE6/>
<HBTYPE7/>
<HBTYPE8/>
<HBTYPE9/>
<HBTYPE10/>
<HBTYPE11/>
<HBTYPE12/>
<HBTYPE13>13</HBTYPE13>
<HBTYPE14/>
<HBTYPE15/>
<HBTYPE>13</HBTYPE>
<P1R>0</P1R>
<P2R>0</P2R>
<P3R>0</P3R>
<P4R>0</P4R>
<P5R>0</P5R>
<P6R>0</P6R>
<P7R>0</P7R>
<P8R>0</P8R>
<REFUSEDTOT>0</REFUSEDTOT>
<REFUSED/>
<WTSHORTFALL/>
<WRENT>100.00</WRENT>
<WTCHARGE>100.00</WTCHARGE>
<WSCHARGE/>
<WPSCHRGE/>
<WSUPCHRG/>
<ChildBen>0.00</ChildBen>
<WTSHORTFALL1/>
<WTSHORTFALLHB/>
<WTSHORTFALLHE/>
<WRENT1>100.00</WRENT1>
<WTCHARGE1>100.00</WTCHARGE1>
<WSCHARGE1/>
<WPSCHRGE1/>
<WSUPCHRG1/>
<ChildBen1>0</ChildBen1>
</Group>
<Group>
<BSa>0</BSa>
<BSb>2</BSb>
<BSc>0</BSc>
<BScm>0</BScm>
<BScf>0</BScf>
<BSd>0</BSd>
<BSdm>0</BSdm>
<BSdf>0</BSdf>
<BSe>0</BSe>
<BSem>0</BSem>
<BSef>0</BSef>
<BSf>0</BSf>
<BSfm>0</BSfm>
<BSff>0</BSff>
<BSfmx>0</BSfmx>
<BSffx>0</BSffx>
<BEDROOMSTAND>2</BEDROOMSTAND>
<BEDMINUSBEDS>0</BEDMINUSBEDS>
<WRENTreduced>100.00</WRENTreduced>
<NonDepDeduct>23.35</NonDepDeduct>
<RENTHB/>
<ChildAllowan>0</ChildAllowan>
<PrsnlAllowan>0</PrsnlAllowan>
<HousBenDisAl>0</HousBenDisAl>
<PAIDHB/>
<HCNETAF/>
<ChldAlloCat1>0</ChldAlloCat1>
<ChldAlloCat2>0</ChldAlloCat2>
<P2NnDepDedct>23.35</P2NnDepDedct>
<P3NnDepDedct>0</P3NnDepDedct>
<P4NnDepDedct>0</P4NnDepDedct>
<P5NnDepDedct>0</P5NnDepDedct>
<P6NnDepDedct>0</P6NnDepDedct>
<P7NnDepDedct>0</P7NnDepDedct>
<P8NnDepDedct>0</P8NnDepDedct>
<DAY>10</DAY>
<MONTH>11</MONTH>
<YEAR>2021</YEAR>
<VDAY>15</VDAY>
<VMONTH>4</VMONTH>
<VYEAR>2021</VYEAR>
<MRCDAY/>
<MRCMONTH/>
<MRCYEAR/>
<PPOSTC1>FY1</PPOSTC1>
<PPOSTC2>1JD</PPOSTC2>
<POSTCODE/>
<POSTCOD2/>
</Group>
</Group>

514
spec/fixtures/softwire_imports/case_logs/893ufj2s-lq77-42m4-rty6-ej09gh585uy1.xml vendored

@ -0,0 +1,514 @@
<Group xmlns="http://data.gov.uk/core/logs/2021-CORE-IR-GN" xmlns:app="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:meta="http://data.gov.uk/core/metadata" xmlns:svc="http://www.w3.org/2007/app" xmlns:xf="http://www.w3.org/2002/xforms" xmlns:xfimpl="http://www.w3.org/2002/xforms/implementation" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xxf="http://orbeon.org/oxf/xml/xforms" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<meta:metadata xmlns:es="http://www.ecmascript.org/" xmlns:xqx="http://www.w3.org/2005/XQueryX" xmlns:XSLT="http://www.w3.org/1999/XSL/Transform/compile">
<meta:form-name>2021-CORE-IR-GN</meta:form-name>
<meta:document-id>893ufj2s-lq77-42m4-rty6-ej09gh585uy1</meta:document-id>
<meta:owner-user-id>e29c492473446dca4d50224f2bb7cf965a261d6f</meta:owner-user-id>
<meta:owner-institution-id>7c5bd5fb549c09a2c55d7cb90d7ba84927e64618</meta:owner-institution-id>
<meta:managing-institution-id>7c5bd5fb549c09a2c55d7cb90d7ba84927e64618</meta:managing-institution-id>
<meta:created-date>2022-04-11T13:46:23.953121Z</meta:created-date>
<meta:modified-date>2022-04-11T13:46:23.953121Z</meta:modified-date>
<meta:status>submitted-valid</meta:status>
<meta:reporting-year>2021</meta:reporting-year>
<meta:upload-method>Manual Entry</meta:upload-method>
<meta:schema assert-valid="true"/>
<meta:rules assert-valid="true"/>
</meta:metadata>
<Group>
<Qdp>Yes</Qdp>
<IRProduct>2 London Living Rent</IRProduct>
<IRProductOther/>
<KeyDate>2021-11-10</KeyDate>
<FORM>300072</FORM>
<Landlord source-value="9">1 Private Registered Provider</Landlord>
<Group>
<_1btenagree>1 This Landlord</_1btenagree>
<_1bifanother/>
<_1bCOREcode/>
</Group>
</Group>
<Group>
<_2a>2 No</_2a>
<Q2b>2 Assured</Q2b>
<Q2ba/>
<_2bTenCode>TEN0021</_2bTenCode>
<_2cYears/>
</Group>
<Group>
<P1Age override-field="">37</P1Age>
<P1AR/>
<P1Sex override-field="">Female</P1Sex>
<P1Eco>1) Full Time</P1Eco>
<P1Eth>1 White: English/Scottish/Welsh/Northern Irish/British</P1Eth>
<P1Nat>1 UK national resident in UK</P1Nat>
<P2Age override-field="">34</P2Age>
<P2AR/>
<P2Sex override-field="">Female</P2Sex>
<P2Rel>Other</P2Rel>
<P2Eco>1) Full Time</P2Eco>
<P3Age override-field=""/>
<P3AR/>
<P3Sex override-field=""/>
<P3Rel/>
<P3Eco/>
<P4Age override-field=""/>
<P4AR/>
<P4Sex override-field=""/>
<P4Rel/>
<P4Eco/>
<P5Age override-field=""/>
<P5AR/>
<P5Sex override-field=""/>
<P5Rel/>
<P5Eco/>
<P6Age override-field=""/>
<P6AR/>
<P6Sex override-field=""/>
<P6Rel/>
<P6Eco/>
<P7Age override-field=""/>
<P7AR/>
<P7Sex override-field=""/>
<P7Rel/>
<P7Eco/>
<P8Age override-field=""/>
<P8AR/>
<P8Sex override-field=""/>
<P8Rel/>
<P8Eco/>
<Group>
<ArmedF>2 No</ArmedF>
<LeftAF/>
<Inj/>
<Preg override-field="">2 No</Preg>
</Group>
<Group>
<Q6Ben>7 UC &#x2013; without housing element (not in receipt of HB and tenants not eligible for housing support e.g. residential care home)</Q6Ben>
</Group>
<Group>
<Q7Ben>3 None</Q7Ben>
<Q8Refused/>
<Q8Money override-field=""/>
<Q8a/>
</Group>
<Group>
<Q9a>8 (Non-violent) relationship breakdown with partner</Q9a>
<Q9aa/>
</Group>
<Group>
<_9b override-field="">2 No</_9b>
<Q10-a/>
<Q10-b/>
<Q10-c/>
<Q10-f/>
<Q10-g>Yes</Q10-g>
<Q10-h/>
<Q10ia>1 Yes</Q10ia>
<Q10ib-1/>
<Q10ib-2/>
<Q10ib-3/>
<Q10ib-4>Yes</Q10ib-4>
<Q10ib-5/>
<Q10ib-6/>
<Q10ib-7/>
<Q10ib-8/>
<Q10ib-9/>
<Q10ib-10/>
<Q11 override-field="">28 Living with friends or family</Q11>
<Q12a>Blackpool</Q12a>
<Q12aONS>E09000009</Q12aONS>
<Q12b override-field="">FY1 1JD</Q12b>
<Q12bnot/>
<Q12c>1 Just moved to Local Authority area</Q12c>
<Q12d>1 Just moved to Local Authority area</Q12d>
</Group>
<Group>
<Q13>1 Not homeless</Q13>
<Q14a>2 No</Q14a>
<Q14b1/>
<Q14b2/>
<Q14b3/>
<Q14b4/>
<Q14b5/>
</Group>
<Group>
<Q15CBL>2 No</Q15CBL>
<Q15CHR>2 No</Q15CHR>
<Q15CAP>2 No</Q15CAP>
</Group>
<Group>
<Q16>10 Other social landlord</Q16>
</Group>
</Group>
<Group>
<Q17>1 Weekly for 52 weeks</Q17>
<Q18ai override-field="">100.00</Q18ai>
<Q18aii override-field=""/>
<Q18aiii override-field=""/>
<Q18aiv override-field=""/>
<Q18av override-field="">100.00</Q18av>
<Q18d/>
<Q18dyes override-field=""/>
<Q19void>2021-04-15</Q19void>
<Q19repair/>
<Q20 override-field="">0</Q20>
<Q21a>PROPREF0042</Q21a>
</Group>
<Group>
<Q22 override-field=""/>
<Q23>1 Flat / maisonette</Q23>
<Q24>1 Purpose built</Q24>
<Q25>2 No</Q25>
<Q26>4 An Intermediate Rent basis</Q26>
<Q27>10 Relet - tenant evicted due to arrears</Q27>
</Group>
<Group>
<Q28Auth>Basingstoke &amp; Deane</Q28Auth>
<Q28ONS>E07000084</Q28ONS>
<Q28pc override-field="">RG21 3HU</Q28pc>
</Group>
<Group>
<F1Age>1</F1Age>
<F2Age>1</F2Age>
<F3Age>0</F3Age>
<F4Age>0</F4Age>
<F5Age>0</F5Age>
<F6Age>0</F6Age>
<F7Age>0</F7Age>
<F8Age>0</F8Age>
<FAge>2</FAge>
<F1>1</F1>
<F2>1</F2>
<F3>0</F3>
<F4>0</F4>
<F5>0</F5>
<F6>0</F6>
<F7>0</F7>
<F8>0</F8>
<F>2</F>
<P1100>0</P1100>
<P2100>0</P2100>
<P3100>0</P3100>
<P4100>0</P4100>
<P5100>0</P5100>
<P6100>0</P6100>
<P7100>0</P7100>
<P8100>0</P8100>
<_100>0</_100>
<P170>0</P170>
<P270>0</P270>
<P370>0</P370>
<P470>0</P470>
<P570>0</P570>
<P670>0</P670>
<P770>0</P770>
<P870>0</P870>
<_70>0</_70>
<P1PT>0</P1PT>
<P2PT>0</P2PT>
<P3PT>0</P3PT>
<P4PT>0</P4PT>
<P5PT>0</P5PT>
<P6PT>0</P6PT>
<P7PT>0</P7PT>
<P8PT>0</P8PT>
<PT>0</PT>
<P1FT>1</P1FT>
<P2FT>1</P2FT>
<P3FT>0</P3FT>
<P4FT>0</P4FT>
<P5FT>0</P5FT>
<P6FT>0</P6FT>
<P7FT>0</P7FT>
<P8FT>0</P8FT>
<FT>2</FT>
<P1Stud>0</P1Stud>
<P2Stud>0</P2Stud>
<P3Stud>0</P3Stud>
<P4Stud>0</P4Stud>
<P5Stud>0</P5Stud>
<P6Stud>0</P6Stud>
<P7Stud>0</P7Stud>
<P8Stud>0</P8Stud>
<Stud>0</Stud>
<P2Child>0</P2Child>
<P3Child>0</P3Child>
<P4Child>0</P4Child>
<P5Child>0</P5Child>
<P6Child>0</P6Child>
<P7Child>0</P7Child>
<P8Child>0</P8Child>
<Child>0</Child>
<P2Partner>0</P2Partner>
<P3Partner>0</P3Partner>
<P4Partner>0</P4Partner>
<P5Partner>0</P5Partner>
<P6Partner>0</P6Partner>
<P7Partner>0</P7Partner>
<P8Partner>0</P8Partner>
<Partner>0</Partner>
<Q1bV1>0</Q1bV1>
<Q1bV2>0</Q1bV2>
<Q1bV3>1</Q1bV3>
<Q1bVT>1</Q1bVT>
<P1Adult>1</P1Adult>
<P2Adult>1</P2Adult>
<P3Adult>0</P3Adult>
<P4Adult>0</P4Adult>
<P5Adult>0</P5Adult>
<P6Adult>0</P6Adult>
<P7Adult>0</P7Adult>
<P8Adult>0</P8Adult>
<PAdultT>2</PAdultT>
<P2PAge>0</P2PAge>
<P3PAge>0</P3PAge>
<P4PAge>0</P4PAge>
<P5PAge>0</P5PAge>
<P6PAge>0</P6PAge>
<P7PAge>0</P7PAge>
<P8PAge>0</P8PAge>
<PAGE>37</PAGE>
<P2ChildAge>0</P2ChildAge>
<P3ChildAge>0</P3ChildAge>
<P4ChildAge>0</P4ChildAge>
<P5ChildAge>0</P5ChildAge>
<P6ChildAge>0</P6ChildAge>
<P7ChildAge>0</P7ChildAge>
<P8ChildAge>0</P8ChildAge>
<ChildAgeMin>0</ChildAgeMin>
<AgeDiff1>37</AgeDiff1>
<AgeDiff2>0</AgeDiff2>
<AgeDiff3>37</AgeDiff3>
<TODAY>2022-04-12Z</TODAY>
<FutureLimit>2022-04-27Z</FutureLimit>
<minmax1/>
<minmax2/>
<minmax3/>
<minmax4/>
<minmax5/>
<minmax6/>
<minmax7/>
<minmax8/>
<minmax9/>
<minmax0/>
<minmax10/>
<minmaxT/>
<Q10av>0</Q10av>
<Q10bv>0</Q10bv>
<Q10cv>0</Q10cv>
<Q10fv>0</Q10fv>
<Q10gv>20</Q10gv>
<Q10hv>0</Q10hv>
<Q10Validate>20</Q10Validate>
<Q2bv>C</Q2bv>
<P2Agev>1</P2Agev>
<P2Sexv>1</P2Sexv>
<P2Relv>1</P2Relv>
<P2Ecov>1</P2Ecov>
<P2valid>4</P2valid>
<P3Agev>0</P3Agev>
<P3Sexv>0</P3Sexv>
<P3Relv>0</P3Relv>
<P3Ecov>0</P3Ecov>
<P3valid>0</P3valid>
<P4Agev>0</P4Agev>
<P4Sexv>0</P4Sexv>
<P4Relv>0</P4Relv>
<P4Ecov>0</P4Ecov>
<P4valid>0</P4valid>
<P5Agev>0</P5Agev>
<P5Sexv>0</P5Sexv>
<P5Relv>0</P5Relv>
<P5Ecov>0</P5Ecov>
<P5valid>0</P5valid>
<P6Agev>0</P6Agev>
<P6Sexv>0</P6Sexv>
<P6Relv>0</P6Relv>
<P6Ecov>0</P6Ecov>
<P6valid>0</P6valid>
<P7Agev>0</P7Agev>
<P7Sexv>0</P7Sexv>
<P7Relv>0</P7Relv>
<P7Ecov>0</P7Ecov>
<P7valid>0</P7valid>
<P8Agev>0</P8Agev>
<P8Sexv>0</P8Sexv>
<P8Relv>0</P8Relv>
<P8Ecov>0</P8Ecov>
<P8valid>0</P8valid>
<Q14b1v>0</Q14b1v>
<Q14b2v>0</Q14b2v>
<Q14b3v>0</Q14b3v>
<Q14b4v>0</Q14b4v>
<Q14b5v>0</Q14b5v>
<Q14bv>0</Q14bv>
<P2Other>1</P2Other>
<P3Other>0</P3Other>
<P4Other>0</P4Other>
<P5Other>0</P5Other>
<P6Other>0</P6Other>
<P7Other>0</P7Other>
<P8Other>0</P8Other>
<Other>1</Other>
<P2ARefused>0</P2ARefused>
<P3ARefused>0</P3ARefused>
<P4ARefused>0</P4ARefused>
<P5ARefused>0</P5ARefused>
<P6ARefused>0</P6ARefused>
<P7ARefused>0</P7ARefused>
<P8ARefused>0</P8ARefused>
<TAREUSED>0</TAREUSED>
<P2RRefused>0</P2RRefused>
<P3RRefused>0</P3RRefused>
<P4RRefused>0</P4RRefused>
<P5RRefused>0</P5RRefused>
<P6RRefused>0</P6RRefused>
<P7RRefused>0</P7RRefused>
<P8RRefused>0</P8RRefused>
<TotRRefused>0</TotRRefused>
<TOTREFUSED>0</TOTREFUSED>
</Group>
<Group>
<TOTADULT>2</TOTADULT>
<NEW_OLD>2 Existing Tenant</NEW_OLD>
<Q18aValid>1</Q18aValid>
<Q18Valid>1</Q18Valid>
<VACDAYS>209</VACDAYS>
<HHMEMB>2</HHMEMB>
<HHTYPEP1A>1</HHTYPEP1A>
<HHTYPEP2A>1</HHTYPEP2A>
<HHTYPEP3A>0</HHTYPEP3A>
<HHTYPEP4A>0</HHTYPEP4A>
<HHTYPEP5A>0</HHTYPEP5A>
<HHTYPEP6A>0</HHTYPEP6A>
<HHTYPEP7A>0</HHTYPEP7A>
<HHTYPEP8A>0</HHTYPEP8A>
<TADULT>2</TADULT>
<HHTYPEP1E>0</HHTYPEP1E>
<HHTYPEP2E>0</HHTYPEP2E>
<HHTYPEP3E>0</HHTYPEP3E>
<HHTYPEP4E>0</HHTYPEP4E>
<HHTYPEP5E>0</HHTYPEP5E>
<HHTYPEP6E>0</HHTYPEP6E>
<HHTYPEP7E>0</HHTYPEP7E>
<HHTYPEP8E>0</HHTYPEP8E>
<TELDER>0</TELDER>
<HHTYPEP1C>0</HHTYPEP1C>
<HHTYPEP2C>0</HHTYPEP2C>
<HHTYPEP3C>0</HHTYPEP3C>
<HHTYPEP4C>0</HHTYPEP4C>
<HHTYPEP5C>0</HHTYPEP5C>
<HHTYPEP6C>0</HHTYPEP6C>
<HHTYPEP7C>0</HHTYPEP7C>
<HHTYPEP8C>0</HHTYPEP8C>
<TCHILD>0</TCHILD>
<HHTYPE>4 = 2 adults</HHTYPE>
<WEEKLYINC>200.00</WEEKLYINC>
<INCOME>200.00</INCOME>
<TYPEHB>13</TYPEHB>
<AFFRATE/>
<Weekinc>200.00</Weekinc>
<LETTYPE>1 Private Registered Provider</LETTYPE>
<PLOACODE/>
<OACODE/>
<GOVREG>E12000008</GOVREG>
<OWNINGORGID>1</OWNINGORGID>
<OWNINGORGNAME>DLUHC</OWNINGORGNAME>
<MANINGORGNAME>DLUHC</MANINGORGNAME>
<HCNUM>655</HCNUM>
<MANHCNUM>655</MANHCNUM>
<LAHA/>
<MANINGORGID>1</MANINGORGID>
<HBTYPE1/>
<HBTYPE2/>
<HBTYPE3/>
<HBTYPE4/>
<HBTYPE5/>
<HBTYPE6/>
<HBTYPE7/>
<HBTYPE8/>
<HBTYPE9/>
<HBTYPE10/>
<HBTYPE11/>
<HBTYPE12/>
<HBTYPE13>13</HBTYPE13>
<HBTYPE14/>
<HBTYPE15/>
<HBTYPE>13</HBTYPE>
<P1R>0</P1R>
<P2R>0</P2R>
<P3R>0</P3R>
<P4R>0</P4R>
<P5R>0</P5R>
<P6R>0</P6R>
<P7R>0</P7R>
<P8R>0</P8R>
<REFUSEDTOT>0</REFUSEDTOT>
<REFUSED/>
<WTSHORTFALL/>
<WRENT>100.00</WRENT>
<WTCHARGE>100.00</WTCHARGE>
<WSCHARGE/>
<WPSCHRGE/>
<WSUPCHRG/>
<ChildBen>0.00</ChildBen>
<WTSHORTFALL1/>
<WTSHORTFALLHB/>
<WTSHORTFALLHE/>
<WRENT1>100.00</WRENT1>
<WTCHARGE1>100.00</WTCHARGE1>
<WSCHARGE1/>
<WPSCHRGE1/>
<WSUPCHRG1/>
<ChildBen1>0</ChildBen1>
</Group>
<Group>
<BSa>0</BSa>
<BSb>2</BSb>
<BSc>0</BSc>
<BScm>0</BScm>
<BScf>0</BScf>
<BSd>0</BSd>
<BSdm>0</BSdm>
<BSdf>0</BSdf>
<BSe>0</BSe>
<BSem>0</BSem>
<BSef>0</BSef>
<BSf>0</BSf>
<BSfm>0</BSfm>
<BSff>0</BSff>
<BSfmx>0</BSfmx>
<BSffx>0</BSffx>
<BEDROOMSTAND>2</BEDROOMSTAND>
<BEDMINUSBEDS>0</BEDMINUSBEDS>
<WRENTreduced>100.00</WRENTreduced>
<NonDepDeduct>23.35</NonDepDeduct>
<RENTHB/>
<ChildAllowan>0</ChildAllowan>
<PrsnlAllowan>0</PrsnlAllowan>
<HousBenDisAl>0</HousBenDisAl>
<PAIDHB/>
<HCNETAF/>
<ChldAlloCat1>0</ChldAlloCat1>
<ChldAlloCat2>0</ChldAlloCat2>
<P2NnDepDedct>23.35</P2NnDepDedct>
<P3NnDepDedct>0</P3NnDepDedct>
<P4NnDepDedct>0</P4NnDepDedct>
<P5NnDepDedct>0</P5NnDepDedct>
<P6NnDepDedct>0</P6NnDepDedct>
<P7NnDepDedct>0</P7NnDepDedct>
<P8NnDepDedct>0</P8NnDepDedct>
<DAY>10</DAY>
<MONTH>11</MONTH>
<YEAR>2021</YEAR>
<VDAY>15</VDAY>
<VMONTH>4</VMONTH>
<VYEAR>2021</VYEAR>
<MRCDAY/>
<MRCMONTH/>
<MRCYEAR/>
<PPOSTC1>FY1</PPOSTC1>
<PPOSTC2>1JD</PPOSTC2>
<POSTCODE/>
<POSTCOD2/>
</Group>
</Group>

9
spec/lib/tasks/data_import_spec.rb

@ -4,13 +4,9 @@ require "rake"
describe "rake core:data_import", type: :task do
subject(:task) { Rake::Task["core:data_import"] }
let(:fixture_path) { "spec/fixtures/softwire_imports/organisations" }
let(:instance_name) { "paas_import_instance" }
let(:type) { "organisation" }
let(:storage_service) { instance_double(StorageService) }
let(:paas_config_service) { instance_double(PaasConfigurationService) }
let(:import_service) { instance_double(Imports::OrganisationImportService) }
before do
Rake.application.rake_require("tasks/data_import")
@ -24,6 +20,10 @@ describe "rake core:data_import", type: :task do
end
context "when importing organisation data" do
let(:type) { "organisation" }
let(:import_service) { instance_double(Imports::OrganisationImportService) }
let(:fixture_path) { "spec/fixtures/softwire_imports/organisations" }
before do
allow(Imports::OrganisationImportService).to receive(:new).and_return(import_service)
end
@ -40,6 +40,7 @@ describe "rake core:data_import", type: :task do
context "when importing user data" do
let(:type) { "user" }
let(:import_service) { instance_double(Imports::UserImportService) }
let(:fixture_path) { "spec/fixtures/softwire_imports/users" }
before do
allow(Imports::UserImportService).to receive(:new).and_return(import_service)

57
spec/lib/tasks/date_import_field_spec.rb

@ -0,0 +1,57 @@
require "rails_helper"
require "rake"
describe "rake core:data_import_field", type: :task do
subject(:task) { Rake::Task["core:data_import_field"] }
let(:instance_name) { "paas_import_instance" }
let(:storage_service) { instance_double(StorageService) }
let(:paas_config_service) { instance_double(PaasConfigurationService) }
before do
Rake.application.rake_require("tasks/data_import_field")
Rake::Task.define_task(:environment)
task.reenable
allow(StorageService).to receive(:new).and_return(storage_service)
allow(PaasConfigurationService).to receive(:new).and_return(paas_config_service)
allow(ENV).to receive(:[])
allow(ENV).to receive(:[]).with("IMPORT_PAAS_INSTANCE").and_return(instance_name)
end
context "when importing a case log field" do
let(:import_service) { instance_double(Imports::CaseLogsFieldImportService) }
let(:fixture_path) { "spec/fixtures/softwire_imports/case_logs" }
before do
allow(Imports::CaseLogsFieldImportService).to receive(:new).and_return(import_service)
allow(import_service).to receive(:update_field)
end
context "and we update the tenant_code field" do
let(:field) { "tenant_code" }
it "properly configures the storage service" do
expect(StorageService).to receive(:new).with(paas_config_service, instance_name)
task.invoke(field, fixture_path)
end
it "calls the expected update method with parameters" do
expect(import_service).to receive(:update_field).with(field, fixture_path)
task.invoke(field, fixture_path)
end
end
it "raises an exception if no parameters are provided" do
expect { task.invoke }.to raise_error(/Usage/)
end
it "raises an exception if a single parameter is provided" do
expect { task.invoke("one_parameter") }.to raise_error(/Usage/)
end
it "raises an exception if the field is not supported" do
expect { task.invoke("random_field", "my_path") }.to raise_error("Field random_field cannot be updated by data_import_field")
end
end
end

84
spec/models/admin_user_spec.rb

@ -1,84 +0,0 @@
require "rails_helper"
RSpec.describe AdminUser, type: :model do
describe "#new" do
it "requires a phone number" do
expect {
described_class.create!(
email: "admin_test@example.com",
password: "password123",
)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "requires a numerical phone number" do
expect {
described_class.create!(
email: "admin_test@example.com",
password: "password123",
phone: "string",
)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "requires an email" do
expect {
described_class.create!(
password: "password123",
phone: "075752137",
)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "requires a password" do
expect {
described_class.create!(
email: "admin_test@example.com",
phone: "075752137",
)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "can be created" do
expect {
described_class.create!(
email: "admin_test@example.com",
password: "password123",
phone: "075752137",
)
}.to change(described_class, :count).by(1)
end
end
describe "paper trail" do
let(:admin_user) { FactoryBot.create(:admin_user) }
it "creates a record of changes to a log" do
expect { admin_user.update!(phone: "09673867853") }.to change(admin_user.versions, :count).by(1)
end
it "allows case logs to be restored to a previous version" do
admin_user.update!(phone: "09673867853")
expect(admin_user.paper_trail.previous_version.phone).to eq("07563867654")
end
it "signing in does not create a new version" do
expect {
admin_user.update!(
last_sign_in_at: Time.zone.now,
current_sign_in_at: Time.zone.now,
current_sign_in_ip: "127.0.0.1",
last_sign_in_ip: "127.0.0.1",
failed_attempts: 3,
unlock_token: "dummy",
locked_at: Time.zone.now,
reset_password_token: "dummy",
reset_password_sent_at: Time.zone.now,
remember_created_at: Time.zone.now,
sign_in_count: 5,
updated_at: Time.zone.now,
)
}.not_to change(admin_user.versions, :count)
end
end
end

96
spec/models/case_log_spec.rb

@ -1871,6 +1871,102 @@ RSpec.describe CaseLog do
FactoryBot.create(:case_log, startdate: Time.utc(2022, 6, 3))
end
context "when searching logs" do
let!(:case_log_to_search) { FactoryBot.create(:case_log, :completed) }
before do
FactoryBot.create_list(:case_log, 5, :completed)
end
describe "#filter_by_id" do
it "allows searching by a log ID" do
result = described_class.filter_by_id(case_log_to_search.id.to_s)
expect(result.count).to eq(1)
expect(result.first.id).to eq case_log_to_search.id
end
end
describe "#filter_by_tenant_code" do
it "allows searching by a Tenant Code" do
result = described_class.filter_by_tenant_code(case_log_to_search.tenant_code)
expect(result.count).to eq(1)
expect(result.first.id).to eq case_log_to_search.id
end
context "when tenant_code has lower case letters" do
let(:matching_tenant_code_lower_case) { case_log_to_search.tenant_code.downcase }
it "allows searching by a Tenant Code" do
result = described_class.filter_by_tenant_code(matching_tenant_code_lower_case)
expect(result.count).to eq(1)
expect(result.first.id).to eq case_log_to_search.id
end
end
end
describe "#filter_by_propcode" do
it "allows searching by a Property Reference" do
result = described_class.filter_by_propcode(case_log_to_search.propcode)
expect(result.count).to eq(1)
expect(result.first.id).to eq case_log_to_search.id
end
context "when propcode has lower case letters" do
let(:matching_propcode_lower_case) { case_log_to_search.propcode.downcase }
it "allows searching by a Property Reference" do
result = described_class.filter_by_propcode(matching_propcode_lower_case)
expect(result.count).to eq(1)
expect(result.first.id).to eq case_log_to_search.id
end
end
end
describe "#filter_by_postcode" do
it "allows searching by a Property Postcode" do
result = described_class.filter_by_postcode(case_log_to_search.postcode_full)
expect(result.count).to eq(1)
expect(result.first.id).to eq case_log_to_search.id
end
end
describe "#search_by" do
it "allows searching using ID" do
result = described_class.search_by(case_log_to_search.id.to_s)
expect(result.count).to eq(1)
expect(result.first.id).to eq case_log_to_search.id
end
it "allows searching using tenancy code" do
result = described_class.search_by(case_log_to_search.tenant_code)
expect(result.count).to eq(1)
expect(result.first.id).to eq case_log_to_search.id
end
it "allows searching by a Property Reference" do
result = described_class.search_by(case_log_to_search.propcode)
expect(result.count).to eq(1)
expect(result.first.id).to eq case_log_to_search.id
end
it "allows searching by a Property Postcode" do
result = described_class.search_by(case_log_to_search.postcode_full)
expect(result.count).to eq(1)
expect(result.first.id).to eq case_log_to_search.id
end
context "when postcode has spaces and lower case letters" do
let(:matching_postcode_lower_case_with_spaces) { case_log_to_search.postcode_full.downcase.chars.insert(3, " ").join }
it "allows searching by a Property Postcode" do
result = described_class.search_by(matching_postcode_lower_case_with_spaces)
expect(result.count).to eq(1)
expect(result.first.id).to eq case_log_to_search.id
end
end
end
end
context "when filtering by year" do
it "allows filtering on a single year" do
expect(described_class.filter_by_years(%w[2021]).count).to eq(2)

12
spec/models/user_spec.rb

@ -85,6 +85,18 @@ RSpec.describe User, type: :model do
)
end
it "does not send a confirmation email to inactive users" do
expect(DeviseNotifyMailer).not_to receive(:confirmation_instructions)
described_class.create!(
name: "unconfirmed_user",
email: "unconfirmed_user@example.com",
password: "password123",
organisation: other_organisation,
role: "data_provider",
active: false,
)
end
context "when the user is a data provider" do
it "cannot assign roles" do
expect(user.assignable_roles).to eq({})

72
spec/requests/auth/passwords_controller_spec.rb

@ -73,78 +73,6 @@ RSpec.describe Auth::PasswordsController, type: :request do
end
end
context "when an admin user" do
let(:admin_user) { FactoryBot.create(:admin_user) }
describe "reset password" do
let(:new_value) { "new-password" }
before do
allow(DeviseNotifyMailer).to receive(:notify_client).and_return(notify_client)
allow(notify_client).to receive(:send_email).and_return(true)
end
it "renders the user edit password view" do
_raw, enc = Devise.token_generator.generate(AdminUser, :reset_password_token)
get "/admin/password/edit?reset_password_token=#{enc}"
expect(page).to have_css("h1", text: I18n.t("user.reset_password"))
end
context "when passwords entered don't match" do
let(:raw) { admin_user.send_reset_password_instructions }
let(:params) do
{
id: admin_user.id,
admin_user: {
password: new_value,
password_confirmation: "something_else",
reset_password_token: raw,
},
}
end
it "shows an error" do
put "/admin/password", headers: headers, params: params
expect(response).to have_http_status(:unprocessable_entity)
expect(page).to have_content("doesn’t match new password")
end
end
context "when passwords is reset" do
let(:raw) { admin_user.send_reset_password_instructions }
let(:params) do
{
id: admin_user.id,
admin_user: {
password: new_value,
password_confirmation: new_value,
reset_password_token: raw,
},
}
end
it "updates the password" do
expect {
put "/admin/password", headers: headers, params: params
admin_user.reload
}.to change(admin_user, :encrypted_password)
end
it "sends you to the 2FA page and does not allow bypassing 2FA code" do
put "/admin/password", headers: headers, params: params
expect(response).to redirect_to("/admin/two-factor-authentication")
get "/admin/case_logs", headers: headers
expect(response).to redirect_to("/admin/two-factor-authentication")
end
it "triggers an email" do
expect(notify_client).to receive(:send_email)
put "/admin/password", headers:, params:
end
end
end
end
context "when a customer support user" do
let(:support_user) { FactoryBot.create(:user, :support) }

147
spec/requests/case_logs_controller_spec.rb

@ -8,7 +8,7 @@ RSpec.describe CaseLogsController, type: :request do
let(:api_password) { "test_password" }
let(:basic_credentials) do
ActionController::HttpAuthentication::Basic
.encode_credentials(api_username, api_password)
.encode_credentials(api_username, api_password)
end
let(:headers) do
@ -184,6 +184,17 @@ RSpec.describe CaseLogsController, type: :request do
expect(page).to have_content("UA984")
end
context "when there are no logs in the database" do
before do
CaseLog.destroy_all
end
it "page has correct title" do
get "/logs", headers: headers, params: {}
expect(page).to have_title("Your organisation (Logs) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end
end
context "when filtering" do
context "with status filter" do
let(:organisation_2) { FactoryBot.create(:organisation) }
@ -308,6 +319,113 @@ RSpec.describe CaseLogsController, type: :request do
expect(page).not_to have_content("Managing organisation")
end
context "when using a search query" do
let(:logs) { FactoryBot.create_list(:case_log, 3, :completed, owning_organisation: user.organisation) }
let(:log_to_search) { FactoryBot.create(:case_log, :completed, owning_organisation: user.organisation) }
let(:log_total_count) { CaseLog.where(owning_organisation: user.organisation).count }
it "has search results in the title" do
get "/logs?search=#{log_to_search.id}", headers: headers, params: {}
expect(page).to have_title("Your organisation (1 log matching ‘#{log_to_search.id}’ of #{log_total_count} total logs) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end
it "shows case logs matching the id" do
get "/logs?search=#{log_to_search.id}", headers: headers, params: {}
expect(page).to have_link(log_to_search.id.to_s)
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
it "shows case logs matching the tenant code" do
get "/logs?search=#{log_to_search.tenant_code}", headers: headers, params: {}
expect(page).to have_link(log_to_search.id.to_s)
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
it "shows case logs matching the property reference" do
get "/logs?search=#{log_to_search.propcode}", headers: headers, params: {}
expect(page).to have_link(log_to_search.id.to_s)
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
it "shows case logs matching the property postcode" do
get "/logs?search=#{log_to_search.postcode_full}", headers: headers, params: {}
expect(page).to have_link(log_to_search.id.to_s)
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
context "when more than one results with matching postcode" do
let!(:matching_postcode_log) { FactoryBot.create(:case_log, :completed, owning_organisation: user.organisation, postcode_full: log_to_search.postcode_full) }
it "displays all matching logs" do
get "/logs?search=#{log_to_search.postcode_full}", headers: headers, params: {}
expect(page).to have_link(log_to_search.id.to_s)
expect(page).to have_link(matching_postcode_log.id.to_s)
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
end
context "when there are more than 1 page of search results" do
let(:postcode) { "XX11YY" }
let(:logs) { FactoryBot.create_list(:case_log, 30, :completed, owning_organisation: user.organisation, postcode_full: postcode) }
let(:log_total_count) { CaseLog.where(owning_organisation: user.organisation).count }
it "has title with pagination details for page 1" do
get "/logs?search=#{logs[0].postcode_full}", headers: headers, params: {}
expect(page).to have_title("Your organisation (#{logs.count} logs matching ‘#{postcode}’ of #{log_total_count} total logs) (page 1 of 2) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end
it "has title with pagination details for page 2" do
get "/logs?search=#{logs[0].postcode_full}&page=2", headers: headers, params: {}
expect(page).to have_title("Your organisation (#{logs.count} logs matching ‘#{postcode}’ of #{log_total_count} total logs) (page 2 of 2) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end
end
context "when search query doesn't match any logs" do
it "doesn't display any logs" do
get "/logs?search=foobar", headers:, params: {}
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
expect(page).not_to have_link(log_to_search.id.to_s)
end
end
context "when search query is empty" do
it "doesn't display any logs" do
get "/logs?search=", headers:, params: {}
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
expect(page).not_to have_link(log_to_search.id.to_s)
end
end
context "when search and filter is present" do
let(:matching_postcode) { log_to_search.postcode_full }
let(:matching_status) { "in_progress" }
let!(:log_matching_filter_and_search) { FactoryBot.create(:case_log, :in_progress, owning_organisation: user.organisation, postcode_full: matching_postcode) }
it "shows only logs matching both search and filters" do
get "/logs?search=#{matching_postcode}&status[]=#{matching_status}", headers: headers, params: {}
expect(page).to have_link(log_matching_filter_and_search.id.to_s)
expect(page).not_to have_link(log_to_search.id.to_s)
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
end
end
context "when there are less than 20 logs" do
before do
get "/logs", headers:, params: {}
@ -348,7 +466,7 @@ RSpec.describe CaseLogsController, type: :request do
end
it "does not have pagination in the title" do
expect(page).to have_title("Logs")
expect(page).to have_title("Your organisation (Logs) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end
it "shows the download csv link" do
@ -424,7 +542,7 @@ RSpec.describe CaseLogsController, type: :request do
end
it "has pagination in the title" do
expect(page).to have_title("Logs (page 1 of 2)")
expect(page).to have_title("Your organisation (Logs) (page 1 of 2) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end
end
@ -449,7 +567,7 @@ RSpec.describe CaseLogsController, type: :request do
end
it "has pagination in the title" do
expect(page).to have_title("Logs (page 2 of 2)")
expect(page).to have_title("Your organisation (Logs) (page 2 of 2) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end
end
end
@ -638,6 +756,27 @@ RSpec.describe CaseLogsController, type: :request do
csv = CSV.parse(response.body)
expect(csv.count).to eq(2)
end
it "dowloads searched logs" do
get "/logs?search=#{case_log.id}", headers:, params: {}
csv = CSV.parse(response.body)
expect(csv.count).to eq(2)
end
context "when both filter and search applied" do
let(:postcode) { "XX1 1TG" }
before do
FactoryBot.create(:case_log, :in_progress, postcode_full: postcode, owning_organisation: organisation)
FactoryBot.create(:case_log, :completed, postcode_full: postcode, owning_organisation: organisation)
end
it "dowloads logs matching both csv and filter logs" do
get "/logs?status[]=completed&search=#{postcode}", headers:, params: {}
csv = CSV.parse(response.body)
expect(csv.count).to eq(2)
end
end
end
context "when there are more than 20 logs" do

111
spec/requests/organisations_controller_spec.rb

@ -379,10 +379,6 @@ RSpec.describe OrganisationsController, type: :request do
get "/organisations/#{organisation.id}/logs", headers:, params: {}
end
it "displays the name of the organisation in the header" do
expect(CGI.unescape_html(response.body)).to match("<span class=\"govuk-caption-l\">#{organisation.name}</span>")
end
it "only shows logs for that organisation" do
expect(page).to have_content("#{number_of_org1_case_logs} total logs")
organisation.case_logs.map(&:id).each do |case_log_id|
@ -407,6 +403,113 @@ RSpec.describe OrganisationsController, type: :request do
expect(page).to have_css(".app-sub-navigation")
expect(page).to have_content("About this organisation")
end
context "when using a search query" do
let(:logs) { FactoryBot.create_list(:case_log, 3, :completed, owning_organisation: user.organisation) }
let(:log_to_search) { FactoryBot.create(:case_log, :completed, owning_organisation: user.organisation) }
let(:log_total_count) { CaseLog.where(owning_organisation: user.organisation).count }
it "has search results in the title" do
get "/organisations/#{organisation.id}/logs?search=#{log_to_search.id}", headers: headers, params: {}
expect(page).to have_title("Your organisation (1 log matching ‘#{log_to_search.id}’ of #{log_total_count} total logs) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end
it "shows case logs matching the id" do
get "/organisations/#{organisation.id}/logs?search=#{log_to_search.id}", headers: headers, params: {}
expect(page).to have_link(log_to_search.id.to_s)
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
it "shows case logs matching the tenant code" do
get "/organisations/#{organisation.id}/logs?search=#{log_to_search.tenant_code}", headers: headers, params: {}
expect(page).to have_link(log_to_search.id.to_s)
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
it "shows case logs matching the property reference" do
get "/organisations/#{organisation.id}/logs?search=#{log_to_search.propcode}", headers: headers, params: {}
expect(page).to have_link(log_to_search.id.to_s)
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
it "shows case logs matching the property postcode" do
get "/organisations/#{organisation.id}/logs?search=#{log_to_search.postcode_full}", headers: headers, params: {}
expect(page).to have_link(log_to_search.id.to_s)
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
context "when more than one results with matching postcode" do
let!(:matching_postcode_log) { FactoryBot.create(:case_log, :completed, owning_organisation: user.organisation, postcode_full: log_to_search.postcode_full) }
it "displays all matching logs" do
get "/organisations/#{organisation.id}/logs?search=#{log_to_search.postcode_full}", headers: headers, params: {}
expect(page).to have_link(log_to_search.id.to_s)
expect(page).to have_link(matching_postcode_log.id.to_s)
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
end
end
context "when there are more than 1 page of search results" do
let(:postcode) { "XX11YY" }
let(:logs) { FactoryBot.create_list(:case_log, 30, :completed, owning_organisation: user.organisation, postcode_full: postcode) }
let(:log_total_count) { CaseLog.where(owning_organisation: user.organisation).count }
it "has title with pagination details for page 1" do
get "/organisations/#{organisation.id}/logs?search=#{logs[0].postcode_full}", headers: headers, params: {}
expect(page).to have_title("Your organisation (#{logs.count} logs matching ‘#{postcode}’ of #{log_total_count} total logs) (page 1 of 2) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end
it "has title with pagination details for page 2" do
get "/organisations/#{organisation.id}/logs?search=#{logs[0].postcode_full}&page=2", headers: headers, params: {}
expect(page).to have_title("Your organisation (#{logs.count} logs matching ‘#{postcode}’ of #{log_total_count} total logs) (page 2 of 2) - Submit social housing lettings and sales data (CORE) - GOV.UK")
end
end
context "when search query doesn't match any logs" do
it "doesn't display any logs" do
get "/organisations/#{organisation.id}/logs?search=foobar", headers:, params: {}
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
expect(page).not_to have_link(log_to_search.id.to_s)
end
end
context "when search query is empty" do
it "doesn't display any logs" do
get "/organisations/#{organisation.id}/logs?search=", headers:, params: {}
logs.each do |log|
expect(page).not_to have_link(log.id.to_s)
end
expect(page).not_to have_link(log_to_search.id.to_s)
end
end
context "when search and filter is present" do
let(:matching_postcode) { log_to_search.postcode_full }
let(:matching_status) { "in_progress" }
let!(:log_matching_filter_and_search) { FactoryBot.create(:case_log, :in_progress, owning_organisation: user.organisation, postcode_full: matching_postcode) }
it "shows only logs matching both search and filters" do
get "/organisations/#{organisation.id}/logs?search=#{matching_postcode}&status[]=#{matching_status}", headers: headers, params: {}
expect(page).to have_content(log_matching_filter_and_search.id)
expect(page).not_to have_content(log_to_search.id)
logs.each do |log|
expect(page).not_to have_content(log.id)
end
end
end
end
end
context "when viewing a specific organisation details" do

41
spec/requests/rack_attack_spec.rb

@ -12,11 +12,8 @@ describe "Rack::Attack" do
let(:devise_notify_mailer) { DeviseNotifyMailer.new }
let(:params) { { user: { email: } } }
let(:admin_params) { { admin_user: { email: admin_email } } }
let(:user) { FactoryBot.create(:user) }
let(:admin_user) { FactoryBot.create(:admin_user) }
let(:email) { user.email }
let(:admin_email) { admin_user.email }
before do
Rack::Attack.enabled = true
@ -40,15 +37,6 @@ describe "Rack::Attack" do
last_response = response
expect(last_response.status).to eq(200)
end
it "does not throttle for an admin user" do
under_limit.times do
post "/admin/password", params: admin_params
follow_redirect!
end
last_response = response
expect(last_response.status).to eq(200)
end
end
context "when the number of requests is at the throttle limit" do
@ -60,26 +48,6 @@ describe "Rack::Attack" do
last_response = response
expect(last_response.status).to eq(200)
end
it "does not throttle for an admin user" do
limit.times do
post "/admin/password", params: admin_params
follow_redirect!
end
last_response = response
expect(last_response.status).to eq(200)
end
it "does not throttle if both endpoints are hit" do
limit.times do
post "/account/password", params: params
follow_redirect!
post "/admin/password", params: admin_params
follow_redirect!
end
last_response = response
expect(last_response.status).to eq(200)
end
end
context "when the number of requests is over the throttle limit" do
@ -91,15 +59,6 @@ describe "Rack::Attack" do
last_response = response
expect(last_response.status).to eq(429)
end
it "throttles for an admin user" do
over_limit.times do
post "/admin/password", params: admin_params
follow_redirect!
end
last_response = response
expect(last_response.status).to eq(429)
end
end
end
end

4
spec/services/exports/case_log_export_service_spec.rb

@ -47,7 +47,7 @@ RSpec.describe Exports::CaseLogExportService do
end
context "and one case log is available for export" do
let!(:case_log) { FactoryBot.create(:case_log, :completed) }
let!(:case_log) { FactoryBot.create(:case_log, :completed, tenancy_code: "BZ757", propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenant_code: "BZ737") }
let(:expected_data_filename) { "core_2021_2022_jan_mar_f0001_inc0001_pt001.xml" }
it "generates a ZIP export file with the expected filename" do
@ -226,7 +226,7 @@ RSpec.describe Exports::CaseLogExportService do
let(:csv_export_file) { File.open("spec/fixtures/exports/case_logs.csv", "r:UTF-8") }
let(:expected_csv_filename) { "export_2022_05_01.csv" }
let(:case_log) { FactoryBot.create(:case_log, :completed) }
let(:case_log) { FactoryBot.create(:case_log, :completed, tenancy_code: "BZ757", propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenant_code: "BZ737") }
it "generates an CSV export file with the expected content" do
expected_content = replace_entity_ids(case_log, csv_export_file.read)

76
spec/services/imports/case_logs_field_import_service_spec.rb

@ -0,0 +1,76 @@
require "rails_helper"
RSpec.describe Imports::CaseLogsFieldImportService do
subject(:import_service) { described_class.new(storage_service, logger) }
let(:storage_service) { instance_double(StorageService) }
let(:logger) { instance_double(ActiveSupport::Logger) }
let(:real_2021_2022_form) { Form.new("config/forms/2021_2022.json", "2021_2022") }
let(:fixture_directory) { "spec/fixtures/softwire_imports/case_logs" }
def open_file(directory, filename)
File.open("#{directory}/#{filename}.xml")
end
before do
# Owning and Managing organisations
FactoryBot.create(:organisation, old_visible_id: "1", provider_type: "PRP")
# Created by users
FactoryBot.create(:user, old_user_id: "c3061a2e6ea0b702e6f6210d5c52d2a92612d2aa")
# Stub the form handler to use the real form
allow(FormHandler.instance).to receive(:get_form).with("2021_2022").and_return(real_2021_2022_form)
WebMock.stub_request(:get, /api.postcodes.io\/postcodes\/LS166FT/)
.to_return(status: 200, body: '{"status":200,"result":{"codes":{"admin_district":"E08000035"}}}', headers: {})
end
context "when updating a specific log value" do
let(:case_log_id) { "0ead17cb-1668-442d-898c-0d52879ff592" }
let(:case_log_file) { open_file(fixture_directory, case_log_id) }
let(:case_log_xml) { Nokogiri::XML(case_log_file) }
let(:remote_folder) { "case_logs" }
let(:field) { "tenant_code" }
before do
# Stub the S3 file listing and download
allow(storage_service).to receive(:list_files)
.and_return(["#{remote_folder}/#{case_log_id}.xml"])
allow(storage_service).to receive(:get_file_io)
.with("#{remote_folder}/#{case_log_id}.xml")
.and_return(case_log_file)
end
context "and the case log was previously imported" do
let(:case_log) { CaseLog.find_by(old_id: case_log_id) }
before do
Imports::CaseLogsImportService.new(storage_service, logger).create_logs(fixture_directory)
case_log_file.rewind
end
it "logs that the tenant_code already has a value and does not update the case_log" do
expect(logger).to receive(:info).with(/Case Log \d+ has a value for tenant_code, skipping update/)
expect { import_service.send(:update_field, field, remote_folder) }
.not_to(change { case_log.reload.tenant_code })
end
end
context "and the case log was previously imported with empty fields" do
let(:case_log) { CaseLog.find_by(old_id: case_log_id) }
before do
Imports::CaseLogsImportService.new(storage_service, logger).create_logs(fixture_directory)
case_log_file.rewind
case_log.update!(tenant_code: nil)
end
it "updates the case_log" do
expect { import_service.send(:update_field, field, remote_folder) }
.to(change { case_log.reload.tenant_code })
end
end
end
end

37
spec/services/imports/case_logs_import_service_spec.rb

@ -30,7 +30,7 @@ RSpec.describe Imports::CaseLogsImportService do
.to_return(status: 200, body: '{"status":200,"result":{"codes":{"admin_district":"E08000035"}}}', headers: {})
end
context "when importing users" do
context "when importing case logs" do
let(:remote_folder) { "case_logs" }
let(:case_log_id) { "0ead17cb-1668-442d-898c-0d52879ff592" }
let(:case_log_id2) { "166fc004-392e-47a8-acb8-1c018734882b" }
@ -66,6 +66,39 @@ RSpec.describe Imports::CaseLogsImportService do
expect { 2.times { case_log_service.create_logs(remote_folder) } }
.to change(CaseLog, :count).by(3)
end
context "when there are status discrepancies" do
let(:case_log_id4) { "893ufj2s-lq77-42m4-rty6-ej09gh585uy1" }
let(:case_log_id5) { "5ybz29dj-l33t-k1l0-hj86-n4k4ma77xkcd" }
let(:case_log_file) { open_file(fixture_directory, case_log_id4) }
let(:case_log_xml) { Nokogiri::XML(case_log_file) }
before do
allow(storage_service).to receive(:get_file_io)
.with("#{remote_folder}/#{case_log_id4}.xml")
.and_return(open_file(fixture_directory, case_log_id4), open_file(fixture_directory, case_log_id4))
allow(storage_service).to receive(:get_file_io)
.with("#{remote_folder}/#{case_log_id5}.xml")
.and_return(open_file(fixture_directory, case_log_id5), open_file(fixture_directory, case_log_id5))
end
it "the logger logs a warning with the case log's old id/filename" do
expect(logger).to receive(:warn).with(/is not completed/).once
expect(logger).to receive(:warn).with(/Case log with old id:#{case_log_id4} is incomplete but status should be complete/).once
case_log_service.send(:create_log, case_log_xml)
end
it "on completion the ids of all logs with status discrepancies are logged in a warning" do
allow(storage_service).to receive(:list_files)
.and_return(%W[#{remote_folder}/#{case_log_id4}.xml #{remote_folder}/#{case_log_id5}.xml])
allow(logger).to receive(:warn).with(/is not completed/)
allow(logger).to receive(:warn).with(/is incomplete but status should be complete/)
expect(logger).to receive(:warn).with(/The following case logs had status discrepancies: \[893ufj2s-lq77-42m4-rty6-ej09gh585uy1, 5ybz29dj-l33t-k1l0-hj86-n4k4ma77xkcd\]/).once
case_log_service.create_logs(remote_folder)
end
end
end
context "when importing a specific log" do
@ -78,6 +111,8 @@ RSpec.describe Imports::CaseLogsImportService do
it "does not import the voiddate" do
allow(logger).to receive(:warn).with(/is not completed/)
allow(logger).to receive(:warn).with(/Case log with old id:#{case_log_id} is incomplete but status should be complete/)
case_log_service.send(:create_log, case_log_xml)
case_log = CaseLog.where(old_id: case_log_id).first

2
spec/services/imports/organisation_la_import_service_spec.rb

@ -8,7 +8,7 @@ RSpec.describe Imports::OrganisationLaImportService do
let(:storage_service) { instance_double(StorageService) }
let(:logger) { instance_double(ActiveSupport::Logger) }
context "when importing data protection confirmations" do
context "when importing organisation las" do
subject(:import_service) { described_class.new(storage_service, logger) }
before do

2
spec/services/imports/organisation_rent_period_import_service_spec.rb

@ -8,7 +8,7 @@ RSpec.describe Imports::OrganisationRentPeriodImportService do
let(:storage_service) { instance_double(StorageService) }
let(:logger) { instance_double(ActiveSupport::Logger) }
context "when importing data protection confirmations" do
context "when importing organisation rent periods" do
subject(:import_service) { described_class.new(storage_service, logger) }
before do

10
webpack.config.js

@ -13,10 +13,6 @@ module.exports = {
entry: {
application: [
"./app/frontend/application.js",
],
active_admin: [
'./app/frontend/active_admin.js',
'./app/frontend/styles/active_admin.scss'
]
},
module: {
@ -68,12 +64,6 @@ module.exports = {
{ from: "app/frontend/vendor/outerHTML.js", to: "vendor" },
{ from: "app/frontend/vendor/polyfill-output-value.js", to: "vendor" }
],
}),
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
jquery: 'jquery',
'window.jQuery': 'jquery'
})
]
}

897
yarn.lock

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