diff --git a/Gemfile b/Gemfile
index c76a48bec..0e5be060d 100644
--- a/Gemfile
+++ b/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"
diff --git a/Gemfile.lock b/Gemfile.lock
index c83c95414..eab0c21a7 100644
--- a/Gemfile.lock
+++ b/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
diff --git a/app/components/bulk_upload_error_row_component.html.erb b/app/components/bulk_upload_error_row_component.html.erb
index 8cfdb674e..4ce6e4f5c 100644
--- a/app/components/bulk_upload_error_row_component.html.erb
+++ b/app/components/bulk_upload_error_row_component.html.erb
@@ -13,7 +13,7 @@
<% if critical_errors.any? %>
Critical errors
These errors must be fixed to complete your logs.
- <%= 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? %>
Confirmation needed
Potential data discrepancies exist in the following cells.
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.
- <%= 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") %>
diff --git a/app/components/bulk_upload_error_row_component.rb b/app/components/bulk_upload_error_row_component.rb
index 1cb4de9d8..569f71b85 100644
--- a/app/components/bulk_upload_error_row_component.rb
+++ b/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
diff --git a/app/components/bulk_upload_error_summary_table_component.html.erb b/app/components/bulk_upload_error_summary_table_component.html.erb
index f9b42f34d..58489612a 100644
--- a/app/components/bulk_upload_error_summary_table_component.html.erb
+++ b/app/components/bulk_upload_error_summary_table_component.html.erb
@@ -3,7 +3,7 @@
<% 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 %>
diff --git a/app/components/bulk_upload_error_summary_table_component.rb b/app/components/bulk_upload_error_summary_table_component.rb
index d15d5280e..db69fd9ec 100644
--- a/app/components/bulk_upload_error_summary_table_component.rb
+++ b/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
diff --git a/app/components/bulk_upload_summary_component.rb b/app/components/bulk_upload_summary_component.rb
index fa4cad414..2df854536 100644
--- a/app/components/bulk_upload_summary_component.rb
+++ b/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
diff --git a/app/components/check_answers_summary_list_card_component.html.erb b/app/components/check_answers_summary_list_card_component.html.erb
index 8b0c7d9b0..c9c974938 100644
--- a/app/components/check_answers_summary_list_card_component.html.erb
+++ b/app/components/check_answers_summary_list_card_component.html.erb
@@ -7,12 +7,12 @@
<% end %>
- <%= 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",
diff --git a/app/components/check_answers_summary_list_card_component.rb b/app/components/check_answers_summary_list_card_component.rb
index 1dc345f01..89345a0eb 100644
--- a/app/components/check_answers_summary_list_card_component.rb
+++ b/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
diff --git a/app/components/create_log_actions_component.html.erb b/app/components/create_log_actions_component.html.erb
index 130072ec0..c8f557bce 100644
--- a/app/components/create_log_actions_component.html.erb
+++ b/app/components/create_log_actions_component.html.erb
@@ -1,11 +1,11 @@
-<%= 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 %>
diff --git a/app/views/bulk_upload_sales_results/summary.html.erb b/app/views/bulk_upload_sales_results/summary.html.erb
index ed2ec14b3..fc9f39d82 100644
--- a/app/views/bulk_upload_sales_results/summary.html.erb
+++ b/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" %>
@@ -34,4 +38,6 @@
<% end %>
-<%= 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 %>
diff --git a/app/views/bulk_upload_sales_resume/confirm.html.erb b/app/views/bulk_upload_sales_resume/confirm.html.erb
index b47619053..1259d8bf9 100644
--- a/app/views/bulk_upload_sales_resume/confirm.html.erb
+++ b/app/views/bulk_upload_sales_resume/confirm.html.erb
@@ -9,7 +9,7 @@
<%= 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) %>
<% if unique_answers_to_be_cleared(@bulk_upload).present? %>
diff --git a/app/views/bulk_upload_sales_resume/fix_choice.html.erb b/app/views/bulk_upload_sales_resume/fix_choice.html.erb
index b376ee62d..8a2887678 100644
--- a/app/views/bulk_upload_sales_resume/fix_choice.html.erb
+++ b/app/views/bulk_upload_sales_resume/fix_choice.html.erb
@@ -19,7 +19,7 @@
- <%= 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) %>
<%= govuk_details(summary_text: "How to choose between fixing errors on the CORE site or in the CSV") do %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index c7d5e2230..3afcddece 100644
--- a/app/views/layouts/application.html.erb
+++ b/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" %>
diff --git a/app/views/layouts/rails_admin/_navigation.html.erb b/app/views/layouts/rails_admin/_navigation.html.erb
index 6c5f77aa3..ef178e4b1 100644
--- a/app/views/layouts/rails_admin/_navigation.html.erb
+++ b/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 %>
diff --git a/app/views/users/_user_list.html.erb b/app/views/users/_user_list.html.erb
index aa686179d..da8aecb0d 100644
--- a/app/views/users/_user_list.html.erb
+++ b/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 %>
diff --git a/docs/Gemfile b/docs/Gemfile
index 27acded18..079c53771 100644
--- a/docs/Gemfile
+++ b/docs/Gemfile
@@ -7,3 +7,8 @@ end
group :development do
gem "webrick"
end
+
+# used to be in standard library
+gem "base64"
+gem "bigdecimal"
+gem "csv"
diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock
index ada074c92..f1c8a1331 100644
--- a/docs/Gemfile.lock
+++ b/docs/Gemfile.lock
@@ -26,6 +26,7 @@ GEM
commonmarker (0.23.10)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
+ csv (3.3.5)
dnsruby (1.61.9)
simpleidn (~> 0.1)
drb (2.2.3)
@@ -275,10 +276,14 @@ GEM
PLATFORMS
arm64-darwin-21
arm64-darwin-23
+ arm64-darwin-25
x86_64-darwin-22
x86_64-linux
DEPENDENCIES
+ base64
+ bigdecimal
+ csv
github-pages
webrick
diff --git a/docs/adr/index.md b/docs/adr/index.md
index b4ee4f8ce..e8479c56e 100644
--- a/docs/adr/index.md
+++ b/docs/adr/index.md
@@ -1,6 +1,6 @@
---
has_children: true
-nav_order: 14
+nav_order: 15
---
# Architecture decisions
diff --git a/docs/dev_tasks/index.md b/docs/dev_tasks/index.md
new file mode 100644
index 000000000..1eeea9e77
--- /dev/null
+++ b/docs/dev_tasks/index.md
@@ -0,0 +1,8 @@
+---
+has_children: true
+nav_order: 14
+---
+
+# Common dev tasks
+
+A collection of guides for tasks that may have to be carried out repeatedly.
diff --git a/docs/dev_tasks/new_question.md b/docs/dev_tasks/new_question.md
new file mode 100644
index 000000000..23fd68f31
--- /dev/null
+++ b/docs/dev_tasks/new_question.md
@@ -0,0 +1,179 @@
+---
+parent: Common dev tasks
+nav_order: 1
+---
+
+# New Questions
+
+Concerns adding a brand-new question to Lettings Logs or Sales Logs. This question will appear on the website as part of the form and should be handled in Bulk Uploads. It will be exported as either a CSV download for users or XML export for automated ingestion by downstream users.
+
+Guide is up-to-date as of 2026.
+
+## Basic checklist of tasks
+
+### 1. Create a migration to add the new field to the database
+
+This allows the answer to the new question to be saved.
+
+You can create a new empty migration file from the terminal if you are in the root of the project:
+
+```
+bin/rails generate migration NameOfMigration
+```
+
+The new migration file will be saved in `db/migrate`.
+
+Whilst the specifics will vary, the new migration file should look something like this:
+
+```ruby
+class AddSexRegisteredAtBirthToSalesLogs < ActiveRecord::Migration[7.2]
+ def change
+ # Add a new column called "name" of type string to the sales_logs table
+ change_table :sales_logs, bulk: true do |t|
+ t.column :name, :string
+ end
+ end
+end
+```
+
+See also: [Active record migrations](https://guides.rubyonrails.org/active_record_migrations.html)
+
+### 2. Run the new migration
+
+`bundle exec rake db:migrate`
+
+This will update `schema.rb`. You should not edit `schema.rb` directly.
+
+### 3. Create a new question class
+
+This will define the question that gets rendered on the online form.
+
+Existing question classes can be found in `app/models/form//questions/`. Depending on the type of question (checkboxes, radio groups, free-text fields), there will almost certainly be an existing question class that you can refer to as a guide.
+
+For example, if you need to create a new radio form, then you may want to copy `armed_forces.rb`.
+
+Sometimes a question will appear in multiple places in the form, or have multiple similar forms. Historically on CORE we'd make a different question class for each, but now we think it's more maintainable to add a small amount of logic to the question to make it dynamic.
+
+See also: [Question]({% link form/question.md %})
+
+### 4. Create a new page class
+
+This creates the page that your new question will be rendered on.
+
+Existing page classes can be found in `app/models/form//pages`.
+
+Usually there is only one question per page, but in some cases there may be multiple. It may not be necessary to create a new page if the new question is being added to an existing one.
+
+See also: [Page]({% link form/page.md %})
+
+### 5. Add new page to an existing subsection
+
+Without this step, your new page will not be inserted into the form!
+
+Subsections can be found in `app/models/form//subsections`.
+
+You will want to add your new page to the appropriate place in the list returned by `def pages`.
+
+To make your new page only appear in the forms for the upcoming year, you wrap the page class in parentheses and add a conditional expression to the end, like so:
+
+```ruby
+(Form::Sales::Pages::SexRegisteredAtBirth1.new(nil, nil, self) if form.start_year_2026_or_later?),
+```
+
+Note: the `@id` attribute of a page is what will be displayed in the url when visiting it. It must be unique within a collection year (i.e. two pages in 25/26 cannot share an ID, but two pages in different collection years can share an ID).
+
+Do not use a `depends_on` block for showing a page on a specific year.
+
+### 6. Update the locale file
+
+The locale files define some of the text for the new question, including hints and the question itself.
+
+Locale files can be found in `config/locales/forms///` and there is one locale file for each form subsection.
+
+Copy the entry for an existing question and substitute in the text for your new one.
+
+The locale config for a question by default is laid out like `en.forms.\.\