Browse Source

Merge branch 'main' into CLDC-4071-show-telephone-extensions-and-export

CLDC-4071-show-telephone-extensions-and-export
Nat Dean-Lewis 5 days ago
parent
commit
e227eff66a
  1. 8
      Gemfile
  2. 103
      Gemfile.lock
  3. 4
      app/components/bulk_upload_error_row_component.html.erb
  4. 9
      app/components/bulk_upload_error_row_component.rb
  5. 4
      app/components/bulk_upload_error_summary_table_component.html.erb
  6. 3
      app/components/bulk_upload_error_summary_table_component.rb
  7. 16
      app/components/bulk_upload_summary_component.rb
  8. 6
      app/components/check_answers_summary_list_card_component.html.erb
  9. 11
      app/components/check_answers_summary_list_card_component.rb
  10. 18
      app/components/create_log_actions_component.html.erb
  11. 19
      app/components/create_log_actions_component.rb
  12. 2
      app/components/data_protection_confirmation_banner_component.html.erb
  13. 5
      app/components/data_protection_confirmation_banner_component.rb
  14. 2
      app/components/document_list_component.html.erb
  15. 2
      app/components/document_list_component.rb
  16. 2
      app/components/lettings_log_summary_component.html.erb
  17. 2
      app/components/lettings_log_summary_component.rb
  18. 2
      app/components/missing_stock_owners_banner_component.html.erb
  19. 9
      app/components/missing_stock_owners_banner_component.rb
  20. 2
      app/components/primary_navigation_component.html.erb
  21. 2
      app/components/primary_navigation_component.rb
  22. 2
      app/components/sales_log_summary_component.html.erb
  23. 2
      app/components/sales_log_summary_component.rb
  24. 4
      app/components/search_component.html.erb
  25. 2
      app/components/search_component.rb
  26. 2
      app/components/search_result_caption_component.rb
  27. 4
      app/components/sub_navigation_component.html.erb
  28. 2
      app/components/sub_navigation_component.rb
  29. 10
      app/controllers/users_controller.rb
  30. 2
      app/frontend/styles/_filter.scss
  31. 11
      app/frontend/styles/_header.scss
  32. 2
      app/frontend/styles/_related-navigation.scss
  33. 2
      app/frontend/styles/_tag.scss
  34. 2
      app/frontend/styles/_testing-tools.scss
  35. 12
      app/frontend/styles/application.scss
  36. 13
      app/helpers/application_helper.rb
  37. 6
      app/models/forms/bulk_upload_resume/confirm.rb
  38. 6
      app/models/forms/bulk_upload_resume/fix_choice.rb
  39. 17
      app/models/user.rb
  40. 6
      app/services/feature_toggle.rb
  41. 4
      app/views/bulk_upload_lettings_results/show.html.erb
  42. 8
      app/views/bulk_upload_lettings_results/summary.html.erb
  43. 2
      app/views/bulk_upload_lettings_resume/confirm.html.erb
  44. 2
      app/views/bulk_upload_lettings_resume/fix_choice.html.erb
  45. 4
      app/views/bulk_upload_sales_results/show.html.erb
  46. 8
      app/views/bulk_upload_sales_results/summary.html.erb
  47. 2
      app/views/bulk_upload_sales_resume/confirm.html.erb
  48. 2
      app/views/bulk_upload_sales_resume/fix_choice.html.erb
  49. 30
      app/views/layouts/application.html.erb
  50. 18
      app/views/layouts/rails_admin/_navigation.html.erb
  51. 4
      app/views/users/_user_list.html.erb
  52. 4
      config/locales/en.yml
  53. 12
      lib/tasks/fix_sales_logs_with_invalid_initialpurchase_lasttransaction.rake
  54. 4
      package.json
  55. 46
      spec/helpers/application_helper_spec.rb
  56. 47
      spec/models/user_spec.rb
  57. 36
      spec/requests/bulk_upload_lettings_results_controller_spec.rb
  58. 36
      spec/requests/bulk_upload_sales_results_controller_spec.rb
  59. 54
      spec/requests/users_controller_spec.rb
  60. 4
      webpack.config.js
  61. 1377
      yarn.lock

8
Gemfile

@ -10,7 +10,7 @@ gem "rails", "~> 7.2.2"
# Use postgresql as the database for Active Record
gem "pg", "~> 1.1"
# Use Puma as the app server
gem "puma", "~> 6.4"
gem "puma", "~> 7.2.1"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails]
@ -18,7 +18,7 @@ gem "jsbundling-rails"
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", ">= 1.4.4", require: false
# GOV UK frontend components
gem "govuk-components", "~> 5.7"
gem "govuk-components", "~> 6.2"
# GOV UK component form builder DSL
gem "govuk_design_system_formbuilder", "~> 5.7"
# Convert Markdown into GOV.UK frontend-styled HTML
@ -40,7 +40,7 @@ gem "devise_two_factor_authentication"
gem "uk_postcode"
# Get rich data from postcode lookups. Wraps postcodes.io
# Use Ruby objects to build reusable markup. A React inspired evolution of the presenter pattern
gem "view_component", "~> 3.9"
gem "view_component", "~> 4.9"
# Use the AWS S3 SDK as storage mechanism
gem "aws-sdk-s3"
# Track changes to models for auditing or versioning.
@ -67,7 +67,7 @@ gem "faker"
gem "method_source", "~> 1.1"
gem "rails_admin", "~> 3.1"
gem "ruby-openai"
gem "sidekiq"
gem "sidekiq", "~> 7.2.4"
gem "sidekiq-cron"
gem "unread"

103
Gemfile.lock

@ -78,8 +78,8 @@ GEM
minitest (>= 5.1, < 6)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
auto_strip_attributes (2.6.0)
activerecord (>= 4.0)
@ -123,7 +123,7 @@ GEM
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (4.0.1)
bigdecimal (4.1.2)
bindex (0.8.1)
bootsnap (1.18.3)
msgpack (~> 1.2)
@ -155,18 +155,21 @@ GEM
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.3.6)
connection_pool (2.5.3)
connection_pool (2.5.5)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
cronex (0.15.0)
tzinfo
unicode (>= 0.4.4.5)
cssbundling-rails (1.4.0)
railties (>= 6.0.0)
csv (3.3.2)
date (3.5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (5.0.3)
devise (5.0.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 7.0)
@ -187,7 +190,7 @@ GEM
drb (2.2.3)
dumb_delegator (1.0.0)
encryptor (3.0.0)
erb (6.0.2)
erb (6.0.4)
erb_lint (0.9.0)
activesupport
better_html (>= 2.0.1)
@ -196,7 +199,7 @@ GEM
rubocop (>= 1)
smart_properties
erubi (1.13.1)
et-orbi (1.2.11)
et-orbi (1.4.0)
tzinfo
event_stream_parser (1.0.0)
excon (0.111.0)
@ -207,24 +210,24 @@ GEM
railties (>= 5.0.0)
faker (3.2.3)
i18n (>= 1.8.11, < 2)
faraday (2.14.1)
faraday (2.14.2)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.1.0)
net-http
faraday-net_http (3.4.3)
net-http (~> 0.5)
ffi (1.16.3)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
fugit (1.12.2)
et-orbi (~> 1.4)
raabro (~> 1.4)
globalid (1.2.1)
globalid (1.3.0)
activesupport (>= 6.1)
govuk-components (5.7.0)
govuk-components (6.2.0)
html-attributes-utils (~> 1.0.0, >= 1.0.0)
pagy (>= 6, < 10)
view_component (>= 3.9, < 3.17)
view_component (>= 4.9, < 4.10)
govuk_design_system_formbuilder (5.7.1)
actionview (>= 6.1)
activemodel (>= 6.1)
@ -241,7 +244,7 @@ GEM
ice_nine (0.11.2)
iniparse (1.5.0)
io-console (0.8.2)
irb (1.17.0)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
@ -249,10 +252,10 @@ GEM
jmespath (1.6.2)
jsbundling-rails (1.3.0)
railties (>= 6.0.0)
json (2.19.2)
json (2.19.8)
json-schema (4.1.1)
addressable (>= 2.8)
jwt (2.8.0)
jwt (3.2.0)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
@ -290,9 +293,9 @@ GEM
msgpack (1.7.2)
multipart-post (2.4.1)
nested_form (0.3.2)
net-http (0.4.1)
uri
net-imap (0.5.7)
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.6.4)
date
net-protocol
net-pop (0.1.2)
@ -301,23 +304,23 @@ GEM
timeout
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.19.1-arm64-darwin)
nio4r (2.7.5)
nokogiri (1.19.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-darwin)
nokogiri (1.19.3-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-gnu)
nokogiri (1.19.3-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-musl)
nokogiri (1.19.3-x86_64-linux-musl)
racc (~> 1.4)
notifications-ruby-client (6.0.0)
jwt (>= 1.5, < 3)
notifications-ruby-client (6.4.0)
jwt (>= 1.5, < 4)
orm_adapter (0.5.0)
overcommit (0.63.0)
childprocess (>= 0.6.3, < 6)
iniparse (~> 1.4)
rexml (~> 3.2)
pagy (9.3.2)
pagy (9.4.0)
paper_trail (15.2.0)
activerecord (>= 6.1)
request_store (~> 1.4)
@ -350,19 +353,19 @@ GEM
psych (5.3.1)
date
stringio
public_suffix (5.0.4)
puma (6.5.0)
public_suffix (7.0.5)
puma (7.2.1)
nio4r (~> 2.0)
pundit (2.3.1)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.20)
rack (3.1.21)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-mini-profiler (3.3.1)
rack (>= 1.2.0)
rack-session (2.1.1)
rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
@ -408,7 +411,7 @@ GEM
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.1)
rake (13.4.2)
randexp (0.1.7)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
@ -419,7 +422,7 @@ GEM
tsort
redcarpet (3.6.0)
redis (4.8.1)
redis-client (0.22.1)
redis-client (0.29.0)
connection_pool
regexp_parser (2.11.3)
reline (0.6.3)
@ -513,10 +516,11 @@ GEM
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.19.0)
sidekiq-cron (1.12.0)
fugit (~> 1.8)
sidekiq-cron (2.4.0)
cronex (>= 0.13.0)
fugit (~> 1.8, >= 1.11.1)
globalid (>= 1.0.1)
sidekiq (>= 6)
sidekiq (>= 6.5.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
@ -530,7 +534,7 @@ GEM
thor (1.4.0)
thread_safe (0.3.6)
timecop (0.9.8)
timeout (0.4.3)
timeout (0.6.1)
tsort (0.2.0)
turbo-rails (2.0.13)
actionpack (>= 7.1.0)
@ -538,17 +542,18 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uk_postcode (2.1.8)
unicode (0.4.4.5)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
unread (0.14.0)
activerecord (>= 6.1)
uri (1.0.4)
uri (1.1.1)
useragent (0.16.11)
view_component (3.10.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
view_component (4.9.0)
actionview (>= 7.1.0)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)
virtus (2.0.0)
axiom-types (~> 0.1)
coercible (~> 1.0)
@ -571,7 +576,7 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.5)
zeitwerk (2.8.2)
PLATFORMS
arm64-darwin
@ -599,7 +604,7 @@ DEPENDENCIES
factory_bot_rails
faker
faraday (>= 2.14.1)
govuk-components (~> 5.7)
govuk-components (~> 6.2)
govuk_design_system_formbuilder (~> 5.7)
govuk_markdown
jsbundling-rails
@ -616,7 +621,7 @@ DEPENDENCIES
possessive
propshaft
pry-byebug
puma (~> 6.4)
puma (~> 7.2.1)
pundit
rack (~> 3.1.20)
rack-attack
@ -634,7 +639,7 @@ DEPENDENCIES
selenium-webdriver
sentry-rails
sentry-ruby
sidekiq
sidekiq (~> 7.2.4)
sidekiq-cron
simplecov
stimulus-rails
@ -643,7 +648,7 @@ DEPENDENCIES
tzinfo-data
uk_postcode
unread
view_component (~> 3.9)
view_component (~> 4.9)
web-console (>= 4.1.0)
webmock

4
app/components/bulk_upload_error_row_component.html.erb

@ -13,7 +13,7 @@
<% if critical_errors.any? %>
<h2 class="govuk-heading-m">Critical errors</h2>
<p class="govuk-body">These errors must be fixed to complete your logs.</p>
<%= govuk_table(html_attributes: { class: potential_errors.any? ? "" : "no-bottom-border" }) do |table| %>
<%= helpers.govuk_table(html_attributes: { class: potential_errors.any? ? "" : "no-bottom-border" }) do |table| %>
<%= table.with_head do |head| %>
<% head.with_row do |row| %>
<% row.with_cell(header: true, text: "Cell") %>
@ -39,7 +39,7 @@
<% if potential_errors.any? %>
<h2 class="govuk-heading-m">Confirmation needed</h2>
<p class="govuk-body">Potential data discrepancies exist in the following cells.<br><br>Please resolve all critical errors and review the cells with data discrepancies before re-uploading the file. Bulk confirmation of potential discrepancies is accessible only after all critical errors have been resolved.</p>
<%= govuk_table(html_attributes: { class: "no-bottom-border" }) do |table| %>
<%= helpers.govuk_table(html_attributes: { class: "no-bottom-border" }) do |table| %>
<%= table.with_head do |head| %>
<% head.with_row do |row| %>
<% row.with_cell(header: true, text: "Cell") %>

9
app/components/bulk_upload_error_row_component.rb

@ -2,9 +2,8 @@ class BulkUploadErrorRowComponent < ViewComponent::Base
attr_reader :bulk_upload_errors
def initialize(bulk_upload_errors:)
super()
@bulk_upload_errors = bulk_upload_errors
super
end
def row
@ -18,7 +17,7 @@ class BulkUploadErrorRowComponent < ViewComponent::Base
def tenant_code_html
return if tenant_code.blank?
content_tag :span, class: "govuk-!-margin-left-3" do
helpers.content_tag :span, class: "govuk-!-margin-left-3" do
"Tenant code: #{tenant_code}"
end
end
@ -30,7 +29,7 @@ class BulkUploadErrorRowComponent < ViewComponent::Base
def purchaser_code_html
return if purchaser_code.blank?
content_tag :span, class: "govuk-!-margin-left-3" do
helpers.content_tag :span, class: "govuk-!-margin-left-3" do
"Purchaser code: #{purchaser_code}"
end
end
@ -42,7 +41,7 @@ class BulkUploadErrorRowComponent < ViewComponent::Base
def property_ref_html
return if property_ref.blank?
content_tag :span, class: "govuk-!-margin-left-3" do
helpers.content_tag :span, class: "govuk-!-margin-left-3" do
"Property reference: #{property_ref}"
end
end

4
app/components/bulk_upload_error_summary_table_component.html.erb

@ -3,7 +3,7 @@
</p>
<% sorted_errors.each do |error| %>
<%= govuk_table do |table| %>
<%= helpers.govuk_table do |table| %>
<%= table.with_head do |head| %>
<% head.with_row do |row| %>
<% row.with_cell(text: question_for_field(error[0][1].to_sym), header: true) %>
@ -13,7 +13,7 @@
<%= table.with_body do |body| %>
<% body.with_row do |row| %>
<% row.with_cell(text: error[0][2].html_safe) %>
<% row.with_cell(text: pluralize(error[1], "error"), numeric: true) %>
<% row.with_cell(text: helpers.pluralize(error[1], "error"), numeric: true) %>
<% end %>
<% end %>
<% end %>

3
app/components/bulk_upload_error_summary_table_component.rb

@ -6,9 +6,8 @@ class BulkUploadErrorSummaryTableComponent < ViewComponent::Base
delegate :question_for_field, to: :row_parser_class
def initialize(bulk_upload:)
super()
@bulk_upload = bulk_upload
super
end
def sorted_errors

16
app/components/bulk_upload_summary_component.rb

@ -2,9 +2,9 @@ class BulkUploadSummaryComponent < ViewComponent::Base
attr_reader :bulk_upload
def initialize(bulk_upload:)
super()
@bulk_upload = bulk_upload
@bulk_upload_errors = bulk_upload.bulk_upload_errors
super
end
def upload_status
@ -27,9 +27,9 @@ class BulkUploadSummaryComponent < ViewComponent::Base
return if count.nil? || count <= 0
text = count > 1 ? (plural_text || singular_text.pluralize(count)) : singular_text
content_tag(:p, class: "govuk-!-font-size-16 govuk-!-margin-bottom-1") do
concat(content_tag(:strong, count))
concat(" #{text}")
helpers.content_tag(:p, class: "govuk-!-font-size-16 govuk-!-margin-bottom-1") do
helpers.concat(helpers.content_tag(:strong, count))
helpers.concat(" #{text}")
end
end
@ -44,11 +44,11 @@ class BulkUploadSummaryComponent < ViewComponent::Base
end
def download_lettings_file_link(bulk_upload)
govuk_link_to "Download file", download_lettings_bulk_upload_path(bulk_upload), class: "govuk-link govuk-!-margin-right-2"
helpers.govuk_link_to "Download file", download_lettings_bulk_upload_path(bulk_upload), class: "govuk-link govuk-!-margin-right-2"
end
def download_sales_file_link(bulk_upload)
govuk_link_to "Download file", download_sales_bulk_upload_path(bulk_upload), class: "govuk-link govuk-!-margin-right-2"
helpers.govuk_link_to "Download file", download_sales_bulk_upload_path(bulk_upload), class: "govuk-link govuk-!-margin-right-2"
end
def view_error_report_link(bulk_upload)
@ -61,12 +61,12 @@ class BulkUploadSummaryComponent < ViewComponent::Base
"bulk_upload_#{bulk_upload.log_type}_result_path"
end
govuk_link_to "View error report", send(path, bulk_upload), class: "govuk-link"
helpers.govuk_link_to "View error report", helpers.send(path, bulk_upload), class: "govuk-link"
end
def view_logs_link(bulk_upload)
return unless bulk_upload.status.to_s == "logs_uploaded_with_errors"
govuk_link_to "View logs with errors", send("#{bulk_upload.log_type}_logs_path", bulk_upload_id: [bulk_upload.id]), class: "govuk-link"
helpers.govuk_link_to "View logs with errors", helpers.send("#{bulk_upload.log_type}_logs_path", bulk_upload_id: [bulk_upload.id]), class: "govuk-link"
end
end

6
app/components/check_answers_summary_list_card_component.html.erb

@ -7,12 +7,12 @@
<% end %>
<div class="govuk-summary-card__content">
<%= govuk_summary_list do |summary_list| %>
<%= helpers.govuk_summary_list do |summary_list| %>
<% applicable_questions.each do |question| %>
<% summary_list.with_row do |row| %>
<% row.with_key { get_question_label(question) } %>
<% row.with_value do %>
<%= simple_format(
<%= helpers.simple_format(
get_answer_label(question),
wrapper_tag: "span",
class: "govuk-!-margin-right-4",
@ -21,7 +21,7 @@
<% extra_value = question.get_extra_check_answer_value(log) %>
<% if extra_value && question.answer_label(log).present? %>
<%= simple_format(
<%= helpers.simple_format(
extra_value,
wrapper_tag: "span",
class: "govuk-!-font-weight-regular app-!-colour-muted",

11
app/components/check_answers_summary_list_card_component.rb

@ -2,12 +2,11 @@ class CheckAnswersSummaryListCardComponent < ViewComponent::Base
attr_reader :questions, :log, :user
def initialize(questions:, log:, user:, correcting_hard_validation: false)
super()
@questions = questions
@log = log
@user = user
@correcting_hard_validation = correcting_hard_validation
super
end
def applicable_questions
@ -34,16 +33,16 @@ class CheckAnswersSummaryListCardComponent < ViewComponent::Base
def action_href(question, log)
referrer = question.displayed_as_answered?(log) ? "check_answers" : "check_answers_new_answer"
send("#{log.log_type}_#{question.page.id}_path", log, referrer:)
helpers.send("#{log.log_type}_#{question.page.id}_path", log, referrer:)
end
def correct_validation_action_href(question, log, _related_question_ids, correcting_hard_validation)
return action_href(question, log) unless correcting_hard_validation
if question.displayed_as_answered?(log)
send("#{log.log_type}_confirm_clear_answer_path", log, question_id: question.id)
helpers.send("#{log.log_type}_confirm_clear_answer_path", log, question_id: question.id)
else
send("#{log.log_type}_#{question.page.id}_path", log, referrer: "check_errors", related_question_ids: request.query_parameters["related_question_ids"], original_page_id: request.query_parameters["original_page_id"])
helpers.send("#{log.log_type}_#{question.page.id}_path", log, referrer: "check_errors", related_question_ids: request.query_parameters["related_question_ids"], original_page_id: request.query_parameters["original_page_id"])
end
end
@ -56,7 +55,7 @@ private
"govuk-link govuk-link--no-visited-state"
end
govuk_link_to question.check_answer_prompt, correct_validation_action_href(question, log, nil, @correcting_hard_validation), class: link_class
helpers.govuk_link_to question.check_answer_prompt, correct_validation_action_href(question, log, nil, @correcting_hard_validation), class: link_class
end
def number_of_buyers

18
app/components/create_log_actions_component.html.erb

@ -1,11 +1,11 @@
<div class="govuk-button-group app-filter-toggle <%= "govuk-!-margin-bottom-6" if display_actions? %>">
<% if display_actions? %>
<%= govuk_button_to create_button_copy, create_button_href, class: "govuk-!-margin-right-3" %>
<%= helpers.govuk_button_to create_button_copy, create_button_href, class: "govuk-!-margin-right-3" %>
<% unless user.support? %>
<%= govuk_button_link_to upload_button_copy, upload_button_href, secondary: true %>
<%= helpers.govuk_button_link_to upload_button_copy, upload_button_href, secondary: true %>
<% end %>
<% if user.support? %>
<%= govuk_button_link_to view_uploads_button_copy, view_uploads_button_href, secondary: true %>
<%= helpers.govuk_button_link_to view_uploads_button_copy, view_uploads_button_href, secondary: true %>
<% end %>
<% if FeatureToggle.create_test_logs_enabled? %>
@ -13,42 +13,42 @@
<span class="govuk-tag app-testing-tools__tag">Testing tools</span>
<span class="govuk-body govuk-body-s">These tools can only be seen and used in testing environments.</span>
<div>
<%= govuk_button_link_to create_test_log_href, class: "govuk-button" do %>
<%= helpers.govuk_button_link_to create_test_log_href, class: "govuk-button" do %>
New <%= current_collection_year_label %> test log
<svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"></path>
</svg>
<% end %>
<% if FeatureToggle.allow_future_form_use? %>
<%= govuk_button_link_to create_next_year_test_log_href, class: "govuk-button" do %>
<%= helpers.govuk_button_link_to create_next_year_test_log_href, class: "govuk-button" do %>
New <%= next_collection_year_label %> test log
<svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"></path>
</svg>
<% end %>
<% end %>
<%= govuk_button_link_to create_setup_test_log_href, class: "govuk-button" do %>
<%= helpers.govuk_button_link_to create_setup_test_log_href, class: "govuk-button" do %>
New <%= current_collection_year_label %> test log (setup only)
<svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"></path>
</svg>
<% end %>
<% if FeatureToggle.allow_future_form_use? %>
<%= govuk_button_link_to create_next_year_setup_test_log_href, class: "govuk-button" do %>
<%= helpers.govuk_button_link_to create_next_year_setup_test_log_href, class: "govuk-button" do %>
New <%= next_collection_year_label %> test log (setup only)
<svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"></path>
</svg>
<% end %>
<% end %>
<%= govuk_button_link_to create_test_bulk_upload_href(2025), class: "govuk-button govuk-button--secondary" do %>
<%= helpers.govuk_button_link_to create_test_bulk_upload_href(2025), class: "govuk-button govuk-button--secondary" do %>
25/26 BU test file
<svg class="govuk-button__start-icon bi bi-download" xmlns="http://www.w3.org/2000/svg" width="18" height="19" fill="currentColor" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" />
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z" />
</svg>
<% end %>
<%= govuk_button_link_to create_test_bulk_upload_href(2026), class: "govuk-button govuk-button--secondary" do %>
<%= helpers.govuk_button_link_to create_test_bulk_upload_href(2026), class: "govuk-button govuk-button--secondary" do %>
26/27 BU test file
<svg class="govuk-button__start-icon bi bi-download" xmlns="http://www.w3.org/2000/svg" width="18" height="19" fill="currentColor" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" />

19
app/components/create_log_actions_component.rb

@ -5,11 +5,10 @@ class CreateLogActionsComponent < ViewComponent::Base
attr_reader :bulk_upload, :user, :log_type
def initialize(user:, log_type:, bulk_upload: nil)
super()
@bulk_upload = bulk_upload
@user = user
@log_type = log_type
super
end
def display_actions?
@ -24,7 +23,7 @@ class CreateLogActionsComponent < ViewComponent::Base
end
def create_button_href
send("#{log_type}_logs_path")
helpers.send("#{log_type}_logs_path")
end
def upload_button_copy
@ -32,23 +31,23 @@ class CreateLogActionsComponent < ViewComponent::Base
end
def upload_button_href
send("bulk_upload_#{log_type}_log_path", id: "start")
helpers.send("bulk_upload_#{log_type}_log_path", id: "start")
end
def create_test_log_href
send("create_test_#{log_type}_log_path")
helpers.send("create_test_#{log_type}_log_path")
end
def create_next_year_test_log_href
send("create_next_year_test_#{log_type}_log_path")
helpers.send("create_next_year_test_#{log_type}_log_path")
end
def create_setup_test_log_href
send("create_setup_test_#{log_type}_log_path")
helpers.send("create_setup_test_#{log_type}_log_path")
end
def create_next_year_setup_test_log_href
send("create_next_year_setup_test_#{log_type}_log_path")
helpers.send("create_next_year_setup_test_#{log_type}_log_path")
end
def current_collection_year_label
@ -60,7 +59,7 @@ class CreateLogActionsComponent < ViewComponent::Base
end
def create_test_bulk_upload_href(year)
send("create_#{year}_test_#{log_type}_bulk_upload_path")
helpers.send("create_#{year}_test_#{log_type}_bulk_upload_path")
end
def view_uploads_button_copy
@ -68,6 +67,6 @@ class CreateLogActionsComponent < ViewComponent::Base
end
def view_uploads_button_href
send("bulk_uploads_#{log_type}_logs_path")
helpers.send("bulk_uploads_#{log_type}_logs_path")
end
end

2
app/components/data_protection_confirmation_banner_component.html.erb

@ -1,5 +1,5 @@
<% if display_banner? %>
<%= govuk_notification_banner(title_text: "Important") do %>
<%= helpers.govuk_notification_banner(title_text: "Important") do %>
<p class="govuk-notification-banner__heading govuk-!-width-full" style="max-width: fit-content">
<%= header_text %>
<p>

5
app/components/data_protection_confirmation_banner_component.rb

@ -4,10 +4,9 @@ class DataProtectionConfirmationBannerComponent < ViewComponent::Base
attr_reader :user, :organisation
def initialize(user:, organisation: nil)
super()
@user = user
@organisation = organisation
super
end
def display_banner?
@ -32,7 +31,7 @@ class DataProtectionConfirmationBannerComponent < ViewComponent::Base
def banner_text
if show_no_dpo_message? || user.is_dpo? || !org_or_user_org.holds_own_stock?
govuk_link_to(
helpers.govuk_link_to(
link_text,
link_href,
class: "govuk-notification-banner__link govuk-!-font-weight-bold",

2
app/components/document_list_component.html.erb

@ -5,7 +5,7 @@
<% items.each do |item| %>
<div class="app-document-list__item">
<dt class="app-document-list__item-title">
<%= govuk_link_to item[:name], item[:href] %>
<%= helpers.govuk_link_to item[:name], item[:href] %>
</dt>
<% if item[:description] %>
<dd class="app-document-list__item-description"><%= item[:description] %></dd>

2
app/components/document_list_component.rb

@ -2,8 +2,8 @@ class DocumentListComponent < ViewComponent::Base
attr_reader :items, :label
def initialize(items:, label:)
super()
@items = items
@label = label
super
end
end

2
app/components/lettings_log_summary_component.html.erb

@ -3,7 +3,7 @@
<div class="govuk-grid-column-two-thirds">
<header class="app-log-summary__header">
<h2 class="app-log-summary__title">
<%= govuk_link_to lettings_log_path(log) do %>
<%= helpers.govuk_link_to lettings_log_path(log) do %>
Log <%= log.id %>
<% end %>
</h2>

2
app/components/lettings_log_summary_component.rb

@ -2,9 +2,9 @@ class LettingsLogSummaryComponent < ViewComponent::Base
attr_reader :current_user, :log
def initialize(current_user:, log:)
super()
@current_user = current_user
@log = log
super
end
def log_status

2
app/components/missing_stock_owners_banner_component.html.erb

@ -1,5 +1,5 @@
<% if display_banner? %>
<%= govuk_notification_banner(title_text: "Important") do %>
<%= helpers.govuk_notification_banner(title_text: "Important") do %>
<p class="govuk-notification-banner__heading govuk-!-width-full" style="max-width: fit-content">
<%= header_text %>
<p>

9
app/components/missing_stock_owners_banner_component.rb

@ -4,10 +4,9 @@ class MissingStockOwnersBannerComponent < ViewComponent::Base
attr_reader :user, :organisation
def initialize(user:, organisation: nil)
super()
@user = user
@organisation = organisation || user.organisation
super
end
def display_banner?
@ -36,7 +35,7 @@ class MissingStockOwnersBannerComponent < ViewComponent::Base
private
def add_stock_owner_link
govuk_link_to(
helpers.govuk_link_to(
"add a stock owner",
stock_owners_add_organisation_path(id: organisation.id),
class: "govuk-notification-banner__link govuk-!-font-weight-bold",
@ -44,7 +43,7 @@ private
end
def contact_helpdesk_link
govuk_link_to(
helpers.govuk_link_to(
"contact the helpdesk",
GlobalConstants::HELPDESK_URL,
class: "govuk-notification-banner__link govuk-!-font-weight-bold",
@ -52,7 +51,7 @@ private
end
def users_link
govuk_link_to(
helpers.govuk_link_to(
"users page",
users_path,
class: "govuk-notification-banner__link govuk-!-font-weight-bold",

2
app/components/primary_navigation_component.html.erb

@ -1,4 +1,4 @@
<%= govuk_service_navigation(navigation_id: "primary-navigation", classes: "app-service-navigation") do |sn|
<%= helpers.govuk_service_navigation(navigation_id: "primary-navigation", classes: "app-service-navigation") do |sn|
items.each do |item|
sn.with_navigation_item(text: item[:text], href: item[:href], classes: "", current: item[:current])
end

2
app/components/primary_navigation_component.rb

@ -2,8 +2,8 @@ class PrimaryNavigationComponent < ViewComponent::Base
attr_reader :items
def initialize(items:)
super()
@items = items
super
end
def highlighted_item?(item, _path)

2
app/components/sales_log_summary_component.html.erb

@ -3,7 +3,7 @@
<div class="govuk-grid-column-two-thirds">
<header class="app-log-summary__header">
<h2 class="app-log-summary__title">
<%= govuk_link_to sales_log_path(log) do %>
<%= helpers.govuk_link_to sales_log_path(log) do %>
Log <%= log.id %>
<% end %>
</h2>

2
app/components/sales_log_summary_component.rb

@ -2,9 +2,9 @@ class SalesLogSummaryComponent < ViewComponent::Base
attr_reader :current_user, :log
def initialize(current_user:, log:)
super()
@current_user = current_user
@log = log
super
end
def log_status

4
app/components/search_component.html.erb

@ -1,4 +1,4 @@
<%= form_with url: path(current_user), method: "get", local: true do |f| %>
<%= helpers.form_with url: path(current_user), method: "get", local: true do |f| %>
<div class="app-search govuk-!-margin-bottom-4">
<%= f.govuk_text_field :search,
form_group: {
@ -11,6 +11,6 @@
class: "app-search__input" %>
<%= f.govuk_submit "Search", class: "app-search__button" %>
<%= govuk_button_link_to "Clear search", path(current_user), secondary: true, class: "app-search__button" %>
<%= helpers.govuk_button_link_to "Clear search", path(current_user), secondary: true, class: "app-search__button" %>
</div>
<% end %>

2
app/components/search_component.rb

@ -2,10 +2,10 @@ class SearchComponent < ViewComponent::Base
attr_reader :current_user, :search_label, :value
def initialize(current_user:, search_label:, value: nil)
super()
@current_user = current_user
@search_label = search_label
@value = value
super
end
def path(current_user)

2
app/components/search_result_caption_component.rb

@ -2,12 +2,12 @@ class SearchResultCaptionComponent < ViewComponent::Base
attr_reader :searched, :count, :item_label, :total_count, :item, :filters_count
def initialize(searched:, count:, item_label:, total_count:, item:, filters_count:)
super()
@searched = searched
@count = count
@item_label = item_label
@total_count = total_count
@item = item
@filters_count = filters_count
super
end
end

4
app/components/sub_navigation_component.html.erb

@ -3,11 +3,11 @@
<% items.each do |item| %>
<% if item.current %>
<li class="app-sub-navigation__item app-sub-navigation__item--current">
<%= govuk_link_to item[:text], item[:href], class: "app-sub-navigation__link", aria: { current: "page" } %>
<%= helpers.govuk_link_to item[:text], item[:href], class: "app-sub-navigation__link", aria: { current: "page" } %>
</li>
<% else %>
<li class="app-sub-navigation__item">
<%= govuk_link_to item[:text], item[:href], class: "app-sub-navigation__link" %>
<%= helpers.govuk_link_to item[:text], item[:href], class: "app-sub-navigation__link" %>
</li>
<% end %>
<% end %>

2
app/components/sub_navigation_component.rb

@ -2,8 +2,8 @@ class SubNavigationComponent < ViewComponent::Base
attr_reader :items
def initialize(items:)
super()
@items = items
super
end
def highlighted_item?(item, _path)

10
app/controllers/users_controller.rb

@ -221,8 +221,14 @@ private
@user.errors.add :phone
end
if user_params.key?(:organisation_id) && user_params[:organisation_id].blank?
@user.errors.add :organisation_id, :blank
if user_params.key?(:organisation_id)
if user_params[:organisation_id].blank?
@user.errors.add :organisation_id, :blank
elsif !@user.role_is_allowed_to_be_in_organisation?(override_organisation_id: user_params[:organisation_id].to_i) && @user.id.present?
# this will also be flagged by the validation in user.rb.
# for convenience we show the error early before they go through the change org flow (involves reassigning logs).
@user.errors.add :organisation_id, I18n.t("validations.user.support_user_in_wrong_organisation.change_organisation")
end
end
end

2
app/frontend/styles/_filter.scss

@ -109,7 +109,7 @@
}
.autocomplete__option__hint {
@include govuk-font(14);
@include govuk-font(16);
word-break: break-all;
}
}

11
app/frontend/styles/_header.scss

@ -1,6 +1,4 @@
.app-header {
border-bottom: govuk-spacing(2) solid $govuk-brand-colour;
.govuk-header__logo {
@include govuk-media-query($from: desktop) {
width: 60%;
@ -21,12 +19,3 @@
}
}
}
.app-header--orange,
.app-header--orange .govuk-header__container {
border-bottom-color: govuk-colour("orange");
}
.app-header__no-border-bottom {
border-bottom: 0;
}

2
app/frontend/styles/_related-navigation.scss

@ -11,7 +11,7 @@
.app-related-navigation__sub-heading {
@include govuk-font(16);
border-top: 1px solid govuk-colour("mid-grey", $legacy: "grey-2");
border-top: 1px solid govuk-colour("mid-grey");
margin: 0;
padding-top: govuk-spacing(3);
}

2
app/frontend/styles/_tag.scss

@ -1,5 +1,5 @@
.app-tag--small {
@include govuk-font(14, $weight: bold);
@include govuk-font(16, $weight: bold);
padding-top: 2px;
padding-right: 6px;
padding-bottom: 2px;

2
app/frontend/styles/_testing-tools.scss

@ -12,7 +12,7 @@
}
.app-testing-tools__tag {
@include govuk-font(14);
@include govuk-font(16);
background-color: #fcd6c3;
margin-top: 0;
margin-bottom: 10px;

12
app/frontend/styles/application.scss

@ -130,3 +130,15 @@ $govuk-breakpoints: (
left: -15px;
position: relative;
}
.app-main-service-navigation {
border-bottom: govuk-spacing(2) solid $govuk-brand-colour;
}
.app-service-navigation--orange {
border-bottom-color: govuk-colour("orange");
}
.app-service-navigation--no-border {
border-bottom: 0;
}

13
app/helpers/application_helper.rb

@ -10,14 +10,11 @@ module ApplicationHelper
end
end
def govuk_header_classes(current_user)
if current_user&.support?
"app-header app-header--orange"
elsif notifications_to_display?
"app-header app-header__no-border-bottom"
else
"app-header"
end
def govuk_service_navigation_classes(current_user)
return "app-service-navigation--orange" if current_user&.support?
return "app-service-navigation--no-border" if notifications_to_display?
""
end
def govuk_phase_banner_tag(current_user)

6
app/models/forms/bulk_upload_resume/confirm.rb

@ -20,11 +20,11 @@ module Forms
send("resume_bulk_upload_#{log_type}_result_path", bulk_upload)
end
def error_report_path
def error_report_path(read_only: false)
if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors?
send("summary_bulk_upload_#{log_type}_result_path", bulk_upload)
send("summary_bulk_upload_#{log_type}_result_path", bulk_upload, hide_upload_button: read_only ? "true" : nil)
else
send("bulk_upload_#{log_type}_result_path", bulk_upload)
send("bulk_upload_#{log_type}_result_path", bulk_upload, hide_upload_button: read_only ? "true" : nil)
end
end

6
app/models/forms/bulk_upload_resume/fix_choice.rb

@ -34,11 +34,11 @@ module Forms
end
end
def error_report_path
def error_report_path(read_only: false)
if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors?
send("summary_bulk_upload_#{log_type}_result_path", bulk_upload)
send("summary_bulk_upload_#{log_type}_result_path", bulk_upload, hide_upload_button: read_only ? "true" : nil)
else
send("bulk_upload_#{log_type}_result_path", bulk_upload)
send("bulk_upload_#{log_type}_result_path", bulk_upload, hide_upload_button: read_only ? "true" : nil)
end
end

17
app/models/user.rb

@ -25,6 +25,7 @@ class User < ApplicationRecord
validates :organisation_id, presence: true
validate :organisation_not_merged
validate :support_user_is_in_correct_organisation
has_paper_trail ignore: %w[last_sign_in_at
current_sign_in_at
@ -384,6 +385,12 @@ class User < ApplicationRecord
end
end
def role_is_allowed_to_be_in_organisation?(override_organisation_id: nil)
return true unless support? && FeatureToggle.support_organisation_allow_list.present?
FeatureToggle.support_organisation_allow_list.include?(override_organisation_id || organisation_id)
end
protected
# Checks whether a password is needed or not. For validations only.
@ -401,6 +408,16 @@ private
end
end
def support_user_is_in_correct_organisation
return if role_is_allowed_to_be_in_organisation?
if role_changed?
errors.add :role, I18n.t("validations.user.support_user_in_wrong_organisation.change_role")
else
errors.add :organisation_id, I18n.t("validations.user.support_user_in_wrong_organisation.change_organisation")
end
end
def send_data_protection_confirmation_reminder
return unless persisted?
return unless is_dpo?

6
app/services/feature_toggle.rb

@ -34,4 +34,10 @@ class FeatureToggle
def self.sales_export_enabled?
Time.zone.now >= Time.zone.local(2025, 4, 1) || (Rails.env.review? || Rails.env.staging?)
end
# IDs of organisations a user must be in to be allowed the support role
# if nil this feature will be disabled
def self.support_organisation_allow_list
[1] if Rails.env.production?
end
end

4
app/views/bulk_upload_lettings_results/show.html.erb

@ -33,4 +33,6 @@
</div>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% if params[:hide_upload_button] != "true" %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% end %>

8
app/views/bulk_upload_lettings_results/summary.html.erb

@ -1,3 +1,7 @@
<% content_for :before_content do %>
<%= govuk_back_link(href: :back) %>
<% end %>
<%= render partial: "bulk_upload_shared/moved_user_banner" %>
<div class="govuk-grid-row">
@ -34,4 +38,6 @@
<% end %>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% if params[:hide_upload_button] != "true" %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% end %>

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

@ -9,7 +9,7 @@
<p class="govuk-body">
<%= logs_and_errors_warning(@bulk_upload) %>
<%= govuk_link_to "View the error report", @form.error_report_path %>
<%= govuk_link_to "View the error report", @form.error_report_path(read_only: true) %>
</p>
<% if unique_answers_to_be_cleared(@bulk_upload).present? %>

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

@ -19,7 +19,7 @@
</div>
<div class="govuk-body">
<%= govuk_link_to "View the error report", @form.error_report_path %>
<%= govuk_link_to "View the error report", @form.error_report_path(read_only: true) %>
</div>
<%= govuk_details(summary_text: "How to choose between fixing errors on the CORE site or in the CSV") do %>

4
app/views/bulk_upload_sales_results/show.html.erb

@ -33,4 +33,6 @@
</div>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% if params[:hide_upload_button] != "true" %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% end %>

8
app/views/bulk_upload_sales_results/summary.html.erb

@ -1,3 +1,7 @@
<% content_for :before_content do %>
<%= govuk_back_link(href: :back) %>
<% end %>
<%= render partial: "bulk_upload_shared/moved_user_banner" %>
<div class="govuk-grid-row">
@ -34,4 +38,6 @@
<% end %>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% if params[:hide_upload_button] != "true" %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path(organisation_id: @bulk_upload.organisation_id) %>
<% end %>

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

@ -9,7 +9,7 @@
<p class="govuk-body">
<%= logs_and_errors_warning(@bulk_upload) %>
<%= govuk_link_to "View the error report", @form.error_report_path %>
<%= govuk_link_to "View the error report", @form.error_report_path(read_only: true) %>
</p>
<% if unique_answers_to_be_cleared(@bulk_upload).present? %>

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

@ -19,7 +19,7 @@
</div>
<div class="govuk-body">
<%= govuk_link_to "View the error report", @form.error_report_path %>
<%= govuk_link_to "View the error report", @form.error_report_path(read_only: true) %>
</div>
<%= govuk_details(summary_text: "How to choose between fixing errors on the CORE site or in the CSV") do %>

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

@ -81,20 +81,24 @@
<%= govuk_skip_link %>
<%= govuk_header(
classes: govuk_header_classes(current_user),
classes: "app-header",
homepage_url: root_path,
navigation_classes: "govuk-header__navigation--end",
) do |component|
component.with_product_name(name: t("service_name"))
unless FeatureToggle.service_moved? || FeatureToggle.service_unavailable?
if current_user.nil?
component.with_navigation_item(text: "Sign in", href: user_session_path)
else
component.with_navigation_item(text: "Your account", href: account_path)
component.with_navigation_item(text: "Sign out", href: destroy_user_session_path)
end
end
end %>
) %>
<%= govuk_service_navigation(
service_name: t("service_name"),
service_url: root_path,
classes: "app-main-service-navigation #{govuk_service_navigation_classes(current_user)}",
) do |component| %>
<% unless FeatureToggle.service_moved? || FeatureToggle.service_unavailable? %>
<% if current_user.nil? %>
<%= component.with_navigation_item(text: "Sign in", href: user_session_path) %>
<% else %>
<%= component.with_navigation_item(text: "Your account", href: account_path) %>
<%= component.with_navigation_item(text: "Sign out", href: destroy_user_session_path) %>
<% end %>
<% end %>
<% end %>
<% if notifications_to_display? %>
<%= render "notifications/notification_banner" %>

18
app/views/layouts/rails_admin/_navigation.html.erb

@ -1,9 +1,13 @@
<%= govuk_header(
classes: "app-header app-header--orange",
classes: "app-header",
homepage_url: Rails.application.routes.url_helpers.root_path,
navigation_classes: "govuk-header__navigation--end",
) do |component|
component.with_product_name(name: t("service_name"))
component.with_navigation_item(text: "Your account", href: Rails.application.routes.url_helpers.account_path)
component.with_navigation_item(text: "Sign out", href: Rails.application.routes.url_helpers.destroy_user_session_path)
end %>
) %>
<%= govuk_service_navigation(
service_name: t("service_name"),
service_url: Rails.application.routes.url_helpers.root_path,
classes: "app-main-service-navigation app-service-navigation--orange",
) do |component| %>
<%= component.with_navigation_item(text: "Your account", href: Rails.application.routes.url_helpers.account_path) %>
<%= component.with_navigation_item(text: "Sign out", href: Rails.application.routes.url_helpers.destroy_user_session_path) %>
<% end %>

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

@ -36,7 +36,7 @@
<% if user.is_data_protection_officer? %>
<%= govuk_tag(
classes: "app-tag--small",
colour: "turquoise",
colour: "teal",
text: "Data protection officer",
) %>
<% else %>
@ -45,7 +45,7 @@
<% if user.is_key_contact? %>
<%= govuk_tag(
classes: "app-tag--small",
colour: "turquoise",
colour: "teal",
text: "Key contact",
) %>
<% else %>

4
config/locales/en.yml

@ -260,6 +260,10 @@ en:
blank: "Enter an email address."
role:
invalid: "Role must be data accessor, data provider or data coordinator."
user:
support_user_in_wrong_organisation:
change_role: "You cannot create a support account type for a user in this organisation. Support accounts should only be created for MHCLG and contractor staff as they are administrator level accounts with access to all organisations' data. Any support accounts for housing organisations would be a data protection breach."
change_organisation: "You cannot move a user with a support account to a non-MHCLG organisation. If you need to move the user, change their role type to data coordinator or data provider."
setup:
saledate:

12
lib/tasks/fix_sales_logs_with_invalid_initialpurchase_lasttransaction.rake

@ -1,13 +1,15 @@
desc "We tightened the validation in 2026 between initial purchase date, last transaction date and sale date so that no two can be equal and initial purchase date < last transaction date < sale date. To avoid invalid logs we clear lasttransaction if it equals saledate and if initialpurchase = lasttransaction we clear both"
desc "We tightened the validation between initial purchase date in 2026, last transaction date and sale date so the two can no longer be equal. To avoid invalid logs we clear initialpurchase if it equals saledate and if initialpurchase = lasttransaction we clear both"
task fix_sales_logs_with_invalid_initialpurchase_lasttransaction: :environment do
lasttransaction_equal_saledate_logs = SalesLog.filter_by_year_or_later(2026).where("lasttransaction = saledate")
initial_purchase_equal_lasttransaction_logs = SalesLog.filter_by_year_or_later(2026).where("initialpurchase = lasttransaction")
lasttransaction_equal_saledate_logs = SalesLog.filter_by_year_or_later(2026).where("lasttransaction = saledate")
puts "Updating #{lasttransaction_equal_saledate_logs.count} logs where lasttransaction = saledate, #{lasttransaction_equal_saledate_logs.map(&:id)}"
lasttransaction_equal_saledate_logs.update!(lasttransaction: nil)
# this one must happen first since this will always result in a log that passes date validations
puts "Updating #{initial_purchase_equal_lasttransaction_logs.count} logs where initialpurchase = lasttransaction, #{initial_purchase_equal_lasttransaction_logs.map(&:id)}"
initial_purchase_equal_lasttransaction_logs.update!(initialpurchase: nil, lasttransaction: nil)
# this one could fail if lasttransaction == saledate == initialpurchase, but the above case will have already reset these logs
puts "Updating #{lasttransaction_equal_saledate_logs.count} logs where lasttransaction = saledate, #{lasttransaction_equal_saledate_logs.map(&:id)}"
lasttransaction_equal_saledate_logs.update!(lasttransaction: nil)
puts "Done"
end

4
package.json

@ -12,7 +12,7 @@
"@ministryofjustice/frontend": "^3.3.0",
"@stimulus/polyfills": "^2.0.0",
"@webcomponents/webcomponentsjs": "^2.6.0",
"@x-govuk/govuk-prototype-components": "^3.0.9",
"@x-govuk/govuk-prototype-components": "^6.0.0",
"accessible-autocomplete": "^2.0.3",
"babel-loader": "^8.2.3",
"babel-plugin-macros": "^3.1.0",
@ -21,7 +21,7 @@
"css-loader": "^6.7.1",
"custom-event-polyfill": "^1.0.7",
"file-loader": "^6.2.0",
"govuk-frontend": "5.7.1",
"govuk-frontend": "6.0.0",
"html5shiv": "^3.7.3",
"intersection-observer": "^0.12.0",
"jquery": "^3.7.1",

46
spec/helpers/application_helper_spec.rb

@ -8,18 +8,50 @@ RSpec.describe ApplicationHelper do
let(:pagy) { nil }
let(:current_user) { FactoryBot.create(:user) }
describe "govuk_header_classes" do
context "with external user" do
it "shows the standard app header" do
expect(govuk_header_classes(current_user)).to eq("app-header")
describe "govuk_service_navigation_classes" do
context "with non-support user" do
context "when no notifications are displayed" do
before do
allow(helper).to receive(:notifications_to_display?).and_return(false)
end
it "returns empty string for blue border (default)" do
expect(helper.govuk_service_navigation_classes(current_user)).to eq("")
end
end
context "when notifications are displayed" do
before do
allow(helper).to receive(:notifications_to_display?).and_return(true)
end
it "returns no-border class to hide the border (notification banner shows instead)" do
expect(helper.govuk_service_navigation_classes(current_user)).to eq("app-service-navigation--no-border")
end
end
end
context "with internal support user" do
context "with support user" do
let(:current_user) { FactoryBot.create(:user, :support) }
it "shows an orange header" do
expect(govuk_header_classes(current_user)).to eq("app-header app-header--orange")
context "when no notifications are displayed" do
before do
allow(helper).to receive(:notifications_to_display?).and_return(false)
end
it "always returns orange class for orange border" do
expect(helper.govuk_service_navigation_classes(current_user)).to eq("app-service-navigation--orange")
end
end
context "when notifications are displayed" do
before do
allow(helper).to receive(:notifications_to_display?).and_return(true)
end
it "still returns orange class (support users always see orange border)" do
expect(helper.govuk_service_navigation_classes(current_user)).to eq("app-service-navigation--orange")
end
end
end
end

47
spec/models/user_spec.rb

@ -540,6 +540,53 @@ RSpec.describe User, type: :model do
.to raise_error(ActiveRecord::RecordInvalid, error_message)
end
end
describe "#support_user_is_in_correct_organisation" do
let(:organisation) { create(:organisation) }
context "when the user is not a support user" do
let(:user) { build(:user, :data_coordinator, organisation:) }
it "is valid regardless of the allow list" do
allow(FeatureToggle).to receive(:support_organisation_allow_list).and_return([999])
expect(user).to be_valid
end
end
context "when the user is a support user" do
let(:user) { build(:user, :support, organisation:) }
context "and the allow list is nil" do
before do
allow(FeatureToggle).to receive(:support_organisation_allow_list).and_return(nil)
end
it "is valid" do
expect(user).to be_valid
end
end
context "and the organisation is in the allow list" do
before do
allow(FeatureToggle).to receive(:support_organisation_allow_list).and_return([organisation.id])
end
it "is valid" do
expect(user).to be_valid
end
end
context "and the organisation is not in the allow list" do
before do
allow(FeatureToggle).to receive(:support_organisation_allow_list).and_return([organisation.id + 1])
end
it "is not valid" do
expect(user).not_to be_valid
end
end
end
end
end
describe "delete" do

36
spec/requests/bulk_upload_lettings_results_controller_spec.rb

@ -82,6 +82,24 @@ RSpec.describe BulkUploadLettingsResultsController, type: :request do
expect(response.body).to include("You moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.")
end
end
context "and user has upload button shown" do
it "displays a link to reupload file" do
get "/lettings-logs/bulk-upload-results/#{bulk_upload.id}/summary"
expect(response.body).to include("Upload your file again")
expect(response.body).to include("/lettings-logs/bulk-upload-logs/start")
end
end
context "and user has upload button hidden" do
it "does not display a link to reupload file" do
get "/lettings-logs/bulk-upload-results/#{bulk_upload.id}/summary?hide_upload_button=true"
expect(response.body).not_to include("Upload your file again")
expect(response.body).not_to include("/lettings-logs/bulk-upload-logs/start")
end
end
end
end
@ -152,5 +170,23 @@ RSpec.describe BulkUploadLettingsResultsController, type: :request do
expect(response.body).to include("You moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.")
end
end
context "and user has upload button shown" do
it "displays a link to reupload file" do
get "/lettings-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response.body).to include("Upload your file again")
expect(response.body).to include("/lettings-logs/bulk-upload-logs/start")
end
end
context "and user has upload button hidden" do
it "does not display a link to reupload file" do
get "/lettings-logs/bulk-upload-results/#{bulk_upload.id}?hide_upload_button=true"
expect(response.body).not_to include("Upload your file again")
expect(response.body).not_to include("/lettings-logs/bulk-upload-logs/start")
end
end
end
end

36
spec/requests/bulk_upload_sales_results_controller_spec.rb

@ -44,6 +44,24 @@ RSpec.describe BulkUploadSalesResultsController, type: :request do
expect(response.body).to include("You moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.")
end
end
context "and user has upload button shown" do
it "displays a link to reupload file" do
get "/sales-logs/bulk-upload-results/#{bulk_upload.id}/summary"
expect(response.body).to include("Upload your file again")
expect(response.body).to include("/sales-logs/bulk-upload-logs/start")
end
end
context "and user has upload button hidden" do
it "does not display a link to reupload file" do
get "/sales-logs/bulk-upload-results/#{bulk_upload.id}/summary?hide_upload_button=true"
expect(response.body).not_to include("Upload your file again")
expect(response.body).not_to include("/sales-logs/bulk-upload-logs/start")
end
end
end
end
@ -127,5 +145,23 @@ RSpec.describe BulkUploadSalesResultsController, type: :request do
expect(response.body).to include("You moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.")
end
end
context "and user has upload button shown" do
it "displays a link to reupload file" do
get "/sales-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response.body).to include("Upload your file again")
expect(response.body).to include("/sales-logs/bulk-upload-logs/start")
end
end
context "and user has upload button hidden" do
it "does not display a link to reupload file" do
get "/sales-logs/bulk-upload-results/#{bulk_upload.id}?hide_upload_button=true"
expect(response.body).not_to include("Upload your file again")
expect(response.body).not_to include("/sales-logs/bulk-upload-logs/start")
end
end
end
end

54
spec/requests/users_controller_spec.rb

@ -76,13 +76,39 @@ RSpec.describe UsersController, type: :request do
end
describe "title link" do
it "routes user to the home page" do
sign_in user
get "/", headers:, params: {}
expect(path).to eq("/")
expect(page).to have_content("Welcome back")
expected_link = "<a class=\"govuk-header__link govuk-header__link--homepage\" href=\"/\">"
expect(CGI.unescape_html(response.body)).to include(expected_link)
context "with a non-support user" do
before do
sign_in user
end
it "has GOV.UK header and service navigation both linking to home page" do
get "/", headers:, params: {}
expect(path).to eq("/")
expect(page).to have_content("Welcome back")
govuk_header_link = '<a class="govuk-header__link govuk-header__homepage-link" href="/">'
expect(CGI.unescape_html(response.body)).to include(govuk_header_link)
expect(page).to have_css(".govuk-service-navigation__link[href='/']", text: "Submit social housing lettings and sales data (CORE)")
end
end
context "with a support user" do
let(:support_user) { create(:user, :support) }
before do
sign_in support_user
end
it "has GOV.UK header and service navigation both linking to home page" do
get "/", headers:, params: {}
follow_redirect!
govuk_header_link = '<a class="govuk-header__link govuk-header__homepage-link" href="/">'
expect(CGI.unescape_html(response.body)).to include(govuk_header_link)
expect(page).to have_css(".govuk-service-navigation__link[href='/']", text: "Submit social housing lettings and sales data (CORE)")
end
end
end
@ -2597,18 +2623,4 @@ RSpec.describe UsersController, type: :request do
end
end
end
describe "title link" do
before do
sign_in user
end
it "routes user to the home page" do
get "/", headers:, params: {}
expect(path).to eq("/")
expect(page).to have_content("Welcome back")
expected_link = "<a class=\"govuk-header__link govuk-header__link--homepage\" href=\"/\">"
expect(CGI.unescape_html(response.body)).to include(expected_link)
end
end
end

4
webpack.config.js

@ -40,8 +40,8 @@ module.exports = {
},
resolve: {
alias: {
'govuk-frontend-styles': path.resolve(__dirname, 'node_modules/govuk-frontend/dist/govuk/all.scss'),
'govuk-prototype-styles': path.resolve(__dirname, 'node_modules/@x-govuk/govuk-prototype-components/x-govuk/all.scss'),
'govuk-frontend-styles': path.resolve(__dirname, 'node_modules/govuk-frontend/dist/govuk/index.scss'),
'govuk-prototype-styles': path.resolve(__dirname, 'node_modules/@x-govuk/govuk-prototype-components/dist/govuk-prototype-components.scss'),
'moj-frontend': path.resolve(__dirname, 'node_modules/@ministryofjustice/frontend/moj/all.js')
},
modules: ['node_modules', 'node_modules/govuk-frontend/dist/govuk']

1377
yarn.lock

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