Browse Source

Merge branch 'refs/heads/main' into CLDC-3740-Replace-you-didnt-answer-with-link

# Conflicts:
#	app/frontend/styles/application.scss
#	config/locales/forms/2025/lettings/tenancy_information.en.yml
CLDC-3740-Replace-you-didnt-answer-with-link
Manny Dinssa 3 days ago
parent
commit
7595a3b7bc
  1. 2
      Dockerfile
  2. 34
      app/frontend/styles/_custom-rails-admin.scss
  3. 1
      app/frontend/styles/application.scss
  4. 12
      app/models/form/lettings/questions/sheltered.rb
  5. 1
      app/models/form/lettings/subsections/property_information.rb
  6. 2
      app/models/form/lettings/subsections/tenancy_information.rb
  7. 58
      app/services/documentation_generator.rb
  8. 3
      app/services/exports/organisation_export_constants.rb
  9. 2
      app/services/exports/organisation_export_service.rb
  10. 9
      app/views/layouts/rails_admin/_navigation.html.erb
  11. 70
      app/views/layouts/rails_admin/application.html.erb
  12. 25
      app/views/rails_admin/main/_submit_buttons.html.erb
  13. 68
      app/views/rails_admin/main/dashboard.html.erb
  14. 21
      app/views/rails_admin/main/delete.html.erb
  15. 187
      app/views/rails_admin/main/index.html.erb
  16. 2
      config/initializers/assets.rb
  17. 6
      config/locales/forms/2025/lettings/property_information.en.yml
  18. 0
      config/locales/validations/lettings/financial.en.yml
  19. 5
      db/migrate/20250110150609_add_collection_year.rb
  20. 3
      db/schema.rb
  21. 1
      lib/tasks/generate_lettings_documentation.rake
  22. 6
      lib/tasks/set_log_validation_collection_year.rake
  23. 1
      spec/fixtures/exports/organisation.xml
  24. 29
      spec/lib/tasks/set_log_validation_collection_year_spec.rb
  25. 18
      spec/models/form/lettings/questions/sheltered_spec.rb
  26. 47
      spec/models/form/lettings/subsections/property_information_spec.rb
  27. 24
      spec/models/form/lettings/subsections/tenancy_information_spec.rb
  28. 12
      spec/services/documentation_generator_spec.rb
  29. 21
      spec/services/exports/organisation_export_service_spec.rb

2
Dockerfile

@ -10,7 +10,7 @@ RUN apk add --update --no-cache tzdata && \
# build-base: compilation tools for bundle # build-base: compilation tools for bundle
# yarn: node package manager # yarn: node package manager
# postgresql-dev: postgres driver and libraries # postgresql-dev: postgres driver and libraries
RUN apk add --no-cache build-base=0.5-r3 busybox=1.36.1-r7 nodejs-current=20.8.1-r0 yarn=1.22.19-r0 postgresql13-dev=13.18-r0 git=2.40.3-r0 bash=5.2.15-r5 RUN apk add --no-cache build-base=0.5-r3 busybox=1.36.1-r7 nodejs-current=20.8.1-r0 yarn=1.22.19-r0 postgresql13-dev=13.18-r0 bash=5.2.15-r5
# Bundler version should be the same version as what the Gemfile.lock was bundled with # Bundler version should be the same version as what the Gemfile.lock was bundled with
RUN gem install bundler:2.3.14 --no-document RUN gem install bundler:2.3.14 --no-document

34
app/frontend/styles/_custom-rails-admin.scss

@ -0,0 +1,34 @@
.rails-admin-sidescroll {
overflow-x: scroll;
}
.rails-admin-description_field {
min-width: 500px;
}
.rails-admin-case_field {
min-width: 500px;
}
.rails-admin-error_message_field {
min-width: 500px;
}
.rails-admin-actions {
min-width: 160px;
ul {
float: right;
}
}
.rails-admin-filters-box {
.filter {
// stylelint-disable-next-line declaration-no-important
display: flex !important;
}
button {
min-width: 20%;
}
}

1
app/frontend/styles/application.scss

@ -49,6 +49,7 @@ $govuk-breakpoints: (
@import "sub-navigation"; @import "sub-navigation";
@import "unread-notification"; @import "unread-notification";
@import "red-link"; @import "red-link";
@import "custom-rails-admin";
// App utilities // App utilities
.app-\!-colour-muted { .app-\!-colour-muted {

12
app/models/form/lettings/questions/sheltered.rb

@ -8,18 +8,18 @@ class Form::Lettings::Questions::Sheltered < ::Form::Question
end end
def answer_options def answer_options
if form.start_year_2024_or_later? if form.start_year_2025_or_later?
{ "1" => { "value" => "Yes – specialist retirement housing" }, { "1" => { "value" => "Yes – sheltered housing for tenants with low support needs" },
"2" => { "value" => "Yes – extra care housing" }, "2" => { "value" => "Yes – extra care housing" },
"5" => { "value" => "Yes – sheltered housing for adults aged under 55 years" }, "7" => { "value" => "Yes - other" },
"6" => { "value" => "Yes – sheltered housing for adults aged 55 years and over who are not retired" },
"3" => { "value" => "No" }, "3" => { "value" => "No" },
"divider" => { "value" => true }, "divider" => { "value" => true },
"4" => { "value" => "Don’t know" } } "4" => { "value" => "Don’t know" } }
else else
{ "2" => { "value" => "Yes – extra care housing" }, { "1" => { "value" => "Yes – specialist retirement housing" },
"1" => { "value" => "Yes – specialist retirement housing" }, "2" => { "value" => "Yes – extra care housing" },
"5" => { "value" => "Yes – sheltered housing for adults aged under 55 years" }, "5" => { "value" => "Yes – sheltered housing for adults aged under 55 years" },
"6" => { "value" => "Yes – sheltered housing for adults aged 55 years and over who are not retired" },
"3" => { "value" => "No" }, "3" => { "value" => "No" },
"divider" => { "value" => true }, "divider" => { "value" => true },
"4" => { "value" => "Don’t know" } } "4" => { "value" => "Don’t know" } }

1
app/models/form/lettings/subsections/property_information.rb

@ -25,6 +25,7 @@ class Form::Lettings::Subsections::PropertyInformation < ::Form::Subsection
Form::Lettings::Pages::VoidDateValueCheck.new(nil, nil, self), Form::Lettings::Pages::VoidDateValueCheck.new(nil, nil, self),
Form::Lettings::Pages::PropertyMajorRepairs.new(nil, nil, self), Form::Lettings::Pages::PropertyMajorRepairs.new(nil, nil, self),
Form::Lettings::Pages::PropertyMajorRepairsValueCheck.new(nil, nil, self), Form::Lettings::Pages::PropertyMajorRepairsValueCheck.new(nil, nil, self),
(Form::Lettings::Pages::ShelteredAccommodation.new(nil, nil, self) if form.start_year_2025_or_later?),
].flatten.compact ].flatten.compact
end end

2
app/models/form/lettings/subsections/tenancy_information.rb

@ -16,7 +16,7 @@ class Form::Lettings::Subsections::TenancyInformation < ::Form::Subsection
Form::Lettings::Pages::TenancyLengthAffordableRent.new(nil, nil, self), Form::Lettings::Pages::TenancyLengthAffordableRent.new(nil, nil, self),
Form::Lettings::Pages::TenancyLengthIntermediateRent.new(nil, nil, self), Form::Lettings::Pages::TenancyLengthIntermediateRent.new(nil, nil, self),
(Form::Lettings::Pages::TenancyLengthPeriodic.new(nil, nil, self) if form.start_year_2024_or_later?), (Form::Lettings::Pages::TenancyLengthPeriodic.new(nil, nil, self) if form.start_year_2024_or_later?),
Form::Lettings::Pages::ShelteredAccommodation.new(nil, nil, self), (Form::Lettings::Pages::ShelteredAccommodation.new(nil, nil, self) unless form.start_year_2025_or_later?),
].flatten.compact ].flatten.compact
end end
end end

58
app/services/documentation_generator.rb

@ -18,14 +18,16 @@ class DocumentationGenerator
next next
end end
validation_source = method(meth).source validation = method(meth)
validation_source = validation.source
file_path = validation.source_location[0]
helper_methods_source = all_helper_methods.map { |helper_method| helper_methods_source = all_helper_methods.map { |helper_method|
if validation_source.include?(helper_method.to_s) if validation_source.include?(helper_method.to_s)
method(helper_method).source method(helper_method).source
end end
}.compact.join("\n") }.compact.join("\n")
response = describe_hard_validation(client, meth, validation_source, helper_methods_source, form) response = describe_hard_validation(client, meth, validation_source, helper_methods_source, form, file_path)
next unless response next unless response
begin begin
@ -41,19 +43,19 @@ class DocumentationGenerator
def describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, log_type) def describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, log_type)
all_validation_methods.each do |meth| all_validation_methods.each do |meth|
if LogValidation.where(validation_name: meth.to_s, bulk_upload_specific: true, from: form.start_date, log_type:).exists? if LogValidation.where(validation_name: meth.to_s, bulk_upload_specific: true, collection_year: "#{form.start_date.year}/#{form.start_date.year + 1}", log_type:).exists?
Rails.logger.info("Validation #{meth} already exists for #{form.start_date.year}") Rails.logger.info("Validation #{meth} already exists for #{form.start_date.year}")
next next
end end
validation = row_parser_class.instance_method(meth)
validation_source = row_parser_class.instance_method(meth).source validation_source = validation.source
helper_methods_source = all_helper_methods.map { |helper_method| helper_methods_source = all_helper_methods.map { |helper_method|
if validation_source.include?(helper_method.to_s) if validation_source.include?(helper_method.to_s)
row_parser_class.instance_method(helper_method).source row_parser_class.instance_method(helper_method).source
end end
}.compact.join("\n") }.compact.join("\n")
response = describe_hard_validation(client, meth, validation_source, helper_methods_source, form) response = describe_hard_validation(client, meth, validation_source, helper_methods_source, form, validation.source_location[0])
next unless response next unless response
begin begin
@ -69,7 +71,7 @@ class DocumentationGenerator
def describe_soft_validations(client, all_validation_methods, all_helper_methods, log_type) def describe_soft_validations(client, all_validation_methods, all_helper_methods, log_type)
validation_descriptions = {} validation_descriptions = {}
all_validation_methods.each do |meth| all_validation_methods[0..5].each do |meth|
validation_source = method(meth).source validation_source = method(meth).source
helper_methods_source = all_helper_methods.map { |helper_method| helper_methods_source = all_helper_methods.map { |helper_method|
if validation_source.include?(helper_method.to_s) if validation_source.include?(helper_method.to_s)
@ -101,8 +103,8 @@ class DocumentationGenerator
private private
def describe_hard_validation(client, meth, validation_source, helper_methods_source, form) def describe_hard_validation(client, meth, validation_source, helper_methods_source, form, file_path)
en_yml = File.read("./config/locales/en.yml") en_yml = File.read(translation_file_path(form, file_path))
begin begin
client.chat( client.chat(
@ -166,14 +168,6 @@ private
required: %w[error_message field], required: %w[error_message field],
}, },
}, },
from: {
type: :number,
description: "the year from which the validation starts. If this validation runs for logs with a startdate after a certain year, specify that year here, only if it is not specified in the validation method, leave this field blank",
},
to: {
type: :number,
description: "the year in which the validation ends. If this validation runs for logs with a startdate before a certain year, specify that year here, only if it is not specified in the validation method, leave this field blank",
},
validation_type: { validation_type: {
type: :string, type: :string,
enum: %w[presence format minimum maximum range inclusion length other], enum: %w[presence format minimum maximum range inclusion length other],
@ -271,8 +265,7 @@ Look at these helper methods where needed to understand what is being checked in
error_message: error["error_message"], error_message: error["error_message"],
case: case_info["case_description"], case: case_info["case_description"],
section: form.get_question(error["field"], nil)&.subsection&.id, section: form.get_question(error["field"], nil)&.subsection&.id,
from: case_info["from"] || "", collection_year: "#{form.start_date.year}/#{form.start_date.year + 1}",
to: case_info["to"] || "",
validation_type: case_info["validation_type"], validation_type: case_info["validation_type"],
hard_soft: "hard", hard_soft: "hard",
other_validated_models: case_info["other_validated_models"]) other_validated_models: case_info["other_validated_models"])
@ -295,8 +288,7 @@ Look at these helper methods where needed to understand what is being checked in
error_message: error["error_message"], error_message: error["error_message"],
case: case_info["case_description"], case: case_info["case_description"],
section: form.get_question(error_field, nil)&.subsection&.id, section: form.get_question(error_field, nil)&.subsection&.id,
from: form.start_date, collection_year: "#{form.start_date.year}/#{form.start_date.year + 1}",
to: form.start_date + 1.year,
validation_type: case_info["validation_type"], validation_type: case_info["validation_type"],
hard_soft: "hard", hard_soft: "hard",
other_validated_models: case_info["other_validated_models"], other_validated_models: case_info["other_validated_models"],
@ -333,7 +325,7 @@ Look at these helper methods where needed to understand what is being checked in
return return
end end
if LogValidation.where(validation_name: validation_depends_on_hash.keys.first, field: page_the_validation_applied_to.questions.first.id, from: form.start_date, log_type:).exists? if LogValidation.where(validation_name: validation_depends_on_hash.keys.first, field: page_the_validation_applied_to.questions.first.id, collection_year: "#{form.start_date.year}/#{form.start_date.year + 1}", log_type:).exists?
Rails.logger.info("Validation #{validation_depends_on_hash.keys.first} already exists for #{page_the_validation_applied_to.questions.first.id} for start year #{form.start_date.year}") Rails.logger.info("Validation #{validation_depends_on_hash.keys.first} already exists for #{page_the_validation_applied_to.questions.first.id} for start year #{form.start_date.year}")
return return
end end
@ -360,12 +352,30 @@ Look at these helper methods where needed to understand what is being checked in
error_message:, error_message:,
case: case_info, case: case_info,
section: form.get_question(page_the_validation_applied_to.questions.first.id, nil)&.subsection&.id, section: form.get_question(page_the_validation_applied_to.questions.first.id, nil)&.subsection&.id,
from: form.start_date, collection_year: "#{form.start_date.year}/#{form.start_date.year + 1}",
to: form.start_date + 1.year,
validation_type: result["validation_type"], validation_type: result["validation_type"],
hard_soft: "soft", hard_soft: "soft",
other_validated_models: result["other_validated_models"]) other_validated_models: result["other_validated_models"])
Rails.logger.info("******** described #{validation_depends_on_hash.keys.first} for #{page_the_validation_applied_to.questions.first.id} ********") Rails.logger.info("******** described #{validation_depends_on_hash.keys.first} for #{page_the_validation_applied_to.questions.first.id} ********")
end end
TRANSLATION_FILE_MAPPINGS = {
"property" => "property_information",
}.freeze
def translation_file_path(form, file_path)
return "./config/locales/validations/#{form.type}/#{form.start_date.year}/bulk_upload.en.yml" if file_path.include?("bulk_upload")
file_name = file_path.split("/").last.gsub("_validations.rb", "")
translation_file_name = TRANSLATION_FILE_MAPPINGS[file_name] || file_name
file_path = "./config/locales/validations/#{form.type}/#{translation_file_name}.en.yml"
return file_path if File.exist?(file_path)
shared_file_path = "./config/locales/validations/#{translation_file_name}.en.yml"
return shared_file_path if File.exist?(shared_file_path)
"./config/locales/en.yml"
end
end end

3
app/services/exports/organisation_export_constants.rb

@ -22,6 +22,7 @@ module Exports::OrganisationExportConstants
"dsa_signed_at", "dsa_signed_at",
"dpo_email", "dpo_email",
"profit_status", "profit_status",
"group" "group",
"status"
] ]
end end

2
app/services/exports/organisation_export_service.rb

@ -65,6 +65,8 @@ module Exports
attribute_hash["available_from"] = organisation.available_from&.iso8601 attribute_hash["available_from"] = organisation.available_from&.iso8601
attribute_hash["profit_status"] = nil # will need update when we add the field to the org attribute_hash["profit_status"] = nil # will need update when we add the field to the org
attribute_hash["group"] = nil # will need update when we add the field to the org attribute_hash["group"] = nil # will need update when we add the field to the org
attribute_hash["status"] = organisation.status
attribute_hash["active"] = attribute_hash["status"] == :active
attribute_hash attribute_hash
end end

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

@ -0,0 +1,9 @@
<%= govuk_header(
classes: "app-header app-header--orange",
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 %>

70
app/views/layouts/rails_admin/application.html.erb

@ -0,0 +1,70 @@
</html><!DOCTYPE html>
<html lang="en" class="govuk-template">
<head>
<title>Admin</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= tag.meta name: "viewport", content: "width=device-width, initial-scale=1" %>
<%= tag.meta property: "og:image", content: asset_path("images/govuk-opengraph-image.png") %>
<%= tag.meta name: "theme-color", content: "#0b0c0c" %>
<%= favicon_link_tag asset_path("images/favicon.ico"), type: nil, sizes: "48x48" %>
<%= favicon_link_tag asset_path("images/favicon.svg"), type: "image/svg+xml", sizes: "any" %>
<%= favicon_link_tag asset_path("images/govuk-icon-mask.svg"), rel: "mask-icon", color: "#0b0c0c", type: nil %>
<%= favicon_link_tag asset_path("images/govuk-icon-180.png"), rel: "apple-touch-icon", type: nil %>
<%= stylesheet_link_tag "application" %>
<%= javascript_include_tag "vendor/html5shiv.min.js" %>
<script>
window.html5.elements = "output";
html5.shivDocument(document);
</script>
<%= javascript_include_tag "vendor/polyfill-output-value.js" %>
<%= javascript_include_tag "vendor/outerHTML.js" %>
<%= javascript_include_tag "application", defer: true %>
<% if content_for?(:head) %>
<%= yield(:head) %>
<% end %>
<%= capybara_lockstep if defined?(Capybara::Lockstep) %>
<% if Rails.env.development? %>
<script>
console.log(<%= session.to_json.html_safe %>)
</script>
<% end %>
<%= render "layouts/rails_admin/head" %>
</head>
<body class="govuk-template__body app-template--wide">
<script>
document.body.className += " js-enabled" + ("noModule" in HTMLScriptElement.prototype ? " govuk-frontend-supported" : "");
</script>
<div data-i18n-options="<%= I18n.t("admin.js").to_json %>" id="admin-js"></div>
<div class="badge bg-warning" id="loading" style="display:none; position:fixed; right:20px; bottom:20px; z-index:100000">
<%= t("admin.loading") %>
</div>
<%= govuk_skip_link %>
<%= render "layouts/rails_admin/navigation" %>
<% feedback_link = govuk_link_to "giving us your feedback (opens in a new tab)", t("feedback_form"), rel: "noreferrer noopener", target: "_blank" %>
<%= govuk_phase_banner(
classes: "#{current_user.present? ? 'no-bottom-border ' : ''}govuk-width-container",
tag: { colour: "orange", text: "Support beta" },
text: "This is a new service – help us improve it by #{feedback_link}".html_safe,
) %>
<div class="govuk-width-container">
<main class="govuk-main-wrapper govuk-main-wrapper--auto-spacing" id="main-content" role="main">
<%= render template: "layouts/rails_admin/content" %>
</main>
</div>
<%= render partial: "layouts/feedback" %>
<%= render partial: "layouts/footer", locals: {
accessibility_statement_path: Rails.application.routes.url_helpers.accessibility_statement_path,
privacy_notice_path: Rails.application.routes.url_helpers.privacy_notice_path,
cookies_path: Rails.application.routes.url_helpers.cookies_path,
} %>
</body>
</html>

25
app/views/rails_admin/main/_submit_buttons.html.erb

@ -0,0 +1,25 @@
<div class="form-actions row justify-content-end my-3">
<div class="col-sm-10">
<input name="return_to" type="<%= :hidden %>" value="<%= (params[:return_to].presence || request.referer) %>">
<button class="govuk-button" data-disable-with="<%= t("admin.form.save") %>" name="_save" type="submit"<%= " disabled" unless @action.enabled? %>>
<i class="fas fa-check"></i>
<%= t("admin.form.save") %>
</button>
<span class="extra_buttons">
<% if @action.enabled? && authorized?(:new, @abstract_model) %>
<button class="govuk-button govuk-button--secondary" data-disable-with="<%= t("admin.form.save_and_add_another") %>" name="_add_another" type="submit">
<%= t("admin.form.save_and_add_another") %>
</button>
<% end %>
<% if @action.enabled? && authorized?(:edit, @abstract_model) %>
<button class="govuk-button govuk-button--secondary" data-disable-with="<%= t("admin.form.save_and_edit") %>" name="_add_edit" type="submit"<%= " disabled" unless @action.enabled? %>>
<%= t("admin.form.save_and_edit") %>
</button>
<% end %>
<button class="govuk-button govuk-button--secondary" data-disable-with="<%= t("admin.form.cancel") %>" formnovalidate="<%= true %>" name="_continue" type="submit">
<i class="fas fa-times"></i>
<%= t("admin.form.cancel") %>
</button>
</span>
</div>
</div>

68
app/views/rails_admin/main/dashboard.html.erb

@ -0,0 +1,68 @@
<% if @abstract_models %>
<table class="table table-condensed table-striped table-hover">
<thead>
<tr>
<th class="shrink model-name">
<%= t "admin.table_headers.model_name" %>
</th>
<th class="shrink last-created">
<%= t "admin.table_headers.last_created" %>
</th>
<th class="records">
<%= t "admin.table_headers.records" %>
</th>
<th class="shrink controls"></th>
</tr>
</thead>
<tbody class="table-group-divider">
<% @abstract_models.each do |abstract_model| %>
<% if authorized? :index, abstract_model %>
<% index_path = index_path(model_name: abstract_model.to_param) %>
<% row_class = "#{cycle('odd', 'even')}#{' link' if index_path} #{abstract_model.param_key}_links" %>
<tr class="<%= row_class %>" data-link="<%= index_path %>">
<% last_created = @most_recent_created[abstract_model.model.name] %>
<% active = last_created.try(:today?) %>
<td>
<span class="show">
<%= link_to abstract_model.config.label_plural, index_path %>
</span>
</td>
<td>
<% if last_created %>
<%= t "admin.misc.time_ago", time: time_ago_in_words(last_created), default: "#{time_ago_in_words(last_created)} #{t('admin.misc.ago')}" %>
<% end %>
</td>
<td>
<% count = @count[abstract_model.model.name] %>
<% percent = if count.positive?
@max <= 1 ? count : ((Math.log(count + 1) * 100.0) / Math.log(@max + 1)).to_i
else
-1
end %>
<div class="<%= active ? "active progress-bar-striped " : "" %>progress" style="margin-bottom:0px">
<div class="bg-<%= get_indicator(percent) %> progress-bar animate-width-to" data-animate-length="<%= ([1.0, percent].max.to_i * 20) %>" data-animate-width-to="<%= [2.0, percent].max.to_i %>%" style="width:2%">
<%= @count[abstract_model.model.name] %>
</div>
</div>
</td>
<td class="last links rails-admin-actions">
<ul class="nav list-inline">
<%= menu_for :collection, abstract_model, nil, true %>
</ul>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<% end %>
<% if @history && authorized?(:history_index) %>
<div class="block" id="block-tables">
<div class="content">
<h2>
<%= t("admin.actions.history_index.menu") %>
</h2>
<%= render partial: "rails_admin/main/dashboard_history" %>
</div>
</div>
<% end %>

21
app/views/rails_admin/main/delete.html.erb

@ -0,0 +1,21 @@
<h4>
<%= t("admin.form.are_you_sure_you_want_to_delete_the_object", model_name: @abstract_model.pretty_name.downcase) %>
<q><strong><%= @model_config.with(object: @object).object_label %></strong></q>
<%= t("admin.form.all_of_the_following_related_items_will_be_deleted") %>
</h4>
<ul>
<%= render partial: "delete_notice", object: @object %>
</ul>
<%= form_for(@object, url: delete_path(model_name: @abstract_model.to_param, id: @object.id), html: { method: "delete" }) do %>
<input name="return_to" type="<%= :hidden %>" value="<%= (params[:return_to].presence || request.referer) %>">
<div class="form-actions">
<button class="govuk-button govuk-button--warning" data-disable-with="<%= t("admin.form.confirmation") %>" type="submit">
<i class="fas fa-check"></i>
<%= t("admin.form.confirmation") %>
</button>
<button class="govuk-button" data-disable-with="<%= t("admin.form.cancel") %>" name="_continue" type="submit">
<i class="fas fa-times"></i>
<%= t("admin.form.cancel") %>
</button>
</div>
<% end %>

187
app/views/rails_admin/main/index.html.erb

@ -0,0 +1,187 @@
<% query = params[:query] %>
<% params = request.params.except(:authenticity_token, :action, :controller, :utf8, :bulk_export) %>
<% params.delete(:query) if params[:query].blank? %>
<% params.delete(:sort_reverse) unless params[:sort_reverse] == "true" %>
<% sort_reverse = params[:sort_reverse] %>
<% sort = params[:sort] %>
<% params.delete(:sort) if params[:sort] == @model_config.list.sort_by.to_s %>
<% export_action = RailsAdmin::Config::Actions.find(:export, { controller:, abstract_model: @abstract_model }) %>
<% export_action = nil unless export_action && authorized?(export_action.authorization_key, @abstract_model) %>
<% description = RailsAdmin.config(@abstract_model.model_name).description %>
<% properties = @model_config.list.with(controller:, view: self, object: @abstract_model.model.new).fields_for_table %>
<% checkboxes = @model_config.list.checkboxes? %>
<% table_table_header_count = begin
count = checkboxes ? 1 : 0
count += properties.count
end %>
<% content_for :contextual_tabs do %>
<% if filterable_fields.present? %>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
<%= t("admin.misc.add_filter") %>
<b class="caret"></b>
</a>
<ul class="dropdown-menu dropdown-menu-end" id="filters">
<% filterable_fields.each do |field| %>
<li>
<a
href="#"
class="dropdown-item"
data-options="<%= field.with(view: self).filter_options.to_json %>">
<%= field.label %>
</a>
</li>
<% end %>
</ul>
</li>
<% end %>
<% if checkboxes %>
<%= bulk_menu %>
<% end %>
<% end %>
<style>
<% properties.select { |p| p.column_width.present? }.each do |property| %>
<%= "#list th.#{property.css_class} { width: #{property.column_width}px; min-width: #{property.column_width}px; }" %>
<%= "#list td.#{property.css_class} { max-width: #{property.column_width}px;}" %>
<% end %>
</style>
<div id="list">
<%= form_tag(index_path(params.except(*%w[page f query])), method: :get) do %>
<div class="card mb-3 p-3 bg-light">
<div class="row rails-admin-filters-box" data-options="<%= ordered_filter_options.to_json %>" id="filters_box"></div>
<hr class="filters_box" style="display:<%= ordered_filters.empty? ? "none" : "block" %>">
<div class="row">
<div class="col-sm-8">
<div class="input-group">
<input class="govuk-input govuk-input--width-20" name="query" placeholder="<%= t("admin.misc.filter") %>" type="search" value="<%= query %>" autocomplete="off">
<div class="govuk-!-margin-left-2">
<button class="govuk-button govuk-!-margin-bottom-0" data-disable-with="<%= "<i class=\"fas fa-sync\"></i>#{t('admin.misc.refresh')}" %>" type="submit">
<i class="fas fa-sync"></i>
<%= t("admin.misc.refresh") %>
</button>
</div>
<div id="remove_filter" title="<%= t("admin.misc.reset_filters") %>">
<button class="govuk-button govuk-button--secondary govuk-!-margin-bottom-0">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<% if @model_config.list.search_help.present? %>
<div class="form-text"><%= @model_config.list.search_help %></div>
<% end %>
</div>
<div class="col-sm-4 text-end">
<% if export_action %>
<%= govuk_button_link_to wording_for(:link, export_action), export_path(params.except("page")), class: "govuk-!-margin-bottom-0" %>
<% end %>
</div>
</div>
</div>
<% end %>
<% unless @model_config.list.scopes.empty? %>
<ul class="nav nav-tabs" id="scope_selector">
<% @model_config.list.scopes.each_with_index do |scope, index| %>
<% scope = "_all" if scope.nil? %>
<li class="nav-item">
<a href="<%= index_path(params.merge(scope:, page: nil)) %>" class="nav-link <%= "active" if scope.to_s == params[:scope] || (params[:scope].blank? && index.zero?) %>">
<%= I18n.t("admin.scopes.#{@abstract_model.to_param}.#{scope}", default: I18n.t("admin.scopes.#{scope}", default: scope.to_s.titleize)) %>
</a>
</li>
<% end %>
</ul>
<% end %>
<%= form_tag bulk_action_path(model_name: @abstract_model.to_param), method: :post, id: "bulk_form", class: %w[form mb-3] do %>
<%= hidden_field_tag :bulk_action %>
<% if description.present? %>
<p>
<strong>
<%= description %>
</strong>
</p>
<% end %>
<div id="sidescroll" class="rails-admin-sidescroll">
<table class="table table-condensed table-striped table-hover">
<thead>
<tr>
<% if checkboxes %>
<th class="shrink sticky">
<input class="toggle" type="checkbox">
</th>
<% end %>
<% properties.each do |property| %>
<% selected = (sort == property.name.to_s) %>
<% if property.sortable %>
<% sort_location = index_path params.except("sort_reverse").except("page").merge(sort: property.name).merge(selected && sort_reverse != "true" ? { sort_reverse: "true" } : {}) %>
<% sort_direction = (if selected
sort_reverse == "true" ? "headerSortUp" : "headerSortDown"
end) %>
<% end %>
<th class="<%= [property.sortable && "header", property.sortable && sort_direction, property.sticky? && "sticky", property.css_class, property.type_css_class].select(&:present?).join(" ") %>" data-href="<%= property.sortable && sort_location %>" rel="tooltip" title="<%= property.hint %>">
<%= property.label %>
</th>
<% end %>
<th class="last shrink"></th>
</tr>
</thead>
<tbody class="table-group-divider">
<% @objects.each do |object| %>
<tr class="<%= @abstract_model.param_key %>_row <%= @model_config.list.with(object:).row_css_class %>">
<% if checkboxes %>
<td class="sticky">
<%= check_box_tag "bulk_ids[]", object.id.to_s, false %>
</td>
<% end %>
<% properties.map { |property| property.bind(:object, object) }.each do |property| %>
<% value = property.pretty_value %>
<%= content_tag(:td, class: [property.sticky? && "sticky", property.css_class, property.type_css_class].select(&:present?).map { |x| "rails-admin-#{x}" }, title: strip_tags(value.to_s)) do %>
<%= value %>
<% end %>
<% end %>
<td class="last links ra-sidescroll-frozen rails-admin-actions">
<ul class="nav list-inline">
<%= menu_for :member, @abstract_model, object, true %>
</ul>
</td>
</tr>
<% end %>
<% if @objects.empty? %>
<tr class="empty_row">
<td colspan="<%= table_table_header_count %>">
<%= I18n.t("admin.actions.index.no_records") %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% if @model_config.list.limited_pagination %>
<div class="row">
<div class="col-md-6">
<%= paginate(@objects, theme: "ra-twitter-bootstrap/without_count", total_pages: Float::INFINITY) %>
</div>
</div>
<% elsif @objects.respond_to?(:total_count) %>
<% total_count = @objects.total_count.to_i %>
<div class="row">
<div class="col-md-6">
<%= paginate(@objects, theme: "ra-twitter-bootstrap") %>
</div>
</div>
<div class="row">
<div class="col-md-6">
<%= link_to(t("admin.misc.show_all"), index_path(params.merge(all: true)), class: "govuk-button govuk-button--secondary") unless total_count > 100 || total_count <= @objects.to_a.size %>
</div>
</div>
<div class="clearfix total-count">
<%= "#{total_count} #{@model_config.pluralize(total_count).downcase}" %>
</div>
<% else %>
<div class="clearfix total-count">
<%= "#{@objects.size} #{@model_config.pluralize(@objects.size).downcase}" %>
</div>
<% end %>
<% end %>
</div>

2
config/initializers/assets.rb

@ -5,3 +5,5 @@ Rails.application.config.assets.version = "1.0"
# Add additional assets to the asset load path. # Add additional assets to the asset load path.
Rails.application.config.assets.paths << Rails.root.join("node_modules/@fortawesome/fontawesome-free/webfonts") Rails.application.config.assets.paths << Rails.root.join("node_modules/@fortawesome/fontawesome-free/webfonts")
Rails.application.config.assets.precompile += %w[rails_admin/rails_admin.css rails_admin/rails_admin.js]

6
config/locales/forms/2025/lettings/property_information.en.yml

@ -146,3 +146,9 @@ en:
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "When were the repairs completed?" question_text: "When were the repairs completed?"
sheltered:
page_header: ""
check_answer_label: "Is this letting in sheltered accommodation?"
hint_text: "Sheltered housing and special retirement housing are for tenants with low-level care and support needs. This typically provides some limited support to enable independent living, such as alarm-based assistance or a scheme manager.</br></br>Extra care housing is for tenants with medium to high care and support needs, often with 24 hour access to support staff provided by an agency registered with the Care Quality Commission."
question_text: "Is this letting in sheltered accommodation?"

0
config/locales/validations/lettings/financial.yml → config/locales/validations/lettings/financial.en.yml

5
db/migrate/20250110150609_add_collection_year.rb

@ -0,0 +1,5 @@
class AddCollectionYear < ActiveRecord::Migration[7.2]
def change
add_column :log_validations, :collection_year, :string
end
end

3
db/schema.rb

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2024_12_06_142944) do ActiveRecord::Schema[7.2].define(version: 2025_01_10_150609) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -450,6 +450,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_06_142944) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "checked" t.boolean "checked"
t.string "collection_year"
end end
create_table "merge_request_organisations", force: :cascade do |t| create_table "merge_request_organisations", force: :cascade do |t|

1
lib/tasks/generate_lettings_documentation.rake

@ -83,6 +83,7 @@ namespace :generate_lettings_documentation do
error_message:, error_message:,
case: validation_description, case: validation_description,
section: form.get_question(field, nil)&.subsection&.id, section: form.get_question(field, nil)&.subsection&.id,
collection_year: "#{form.start_date.year}/#{form.start_date.year + 1}",
validation_type: validation_name, validation_type: validation_name,
hard_soft: "hard") hard_soft: "hard")
end end

6
lib/tasks/set_log_validation_collection_year.rake

@ -0,0 +1,6 @@
desc "Sets value for collection_year log validations depending on the from value"
task set_log_validation_collection_year: :environment do
LogValidation.all.each do |log_validation|
log_validation.update(collection_year: "#{log_validation.from.year}/#{log_validation.from.year + 1}")
end
end

1
spec/fixtures/exports/organisation.xml vendored

@ -22,5 +22,6 @@
<dpo_email>{dpo_email}</dpo_email> <dpo_email>{dpo_email}</dpo_email>
<profit_status/> <profit_status/>
<group/> <group/>
<status>active</status>
</form> </form>
</forms> </forms>

29
spec/lib/tasks/set_log_validation_collection_year_spec.rb

@ -0,0 +1,29 @@
require "rails_helper"
require "rake"
RSpec.describe "set_log_validation_collection_year" do
describe ":set_log_validation_collection_year", type: :task do
subject(:task) { Rake::Task["set_log_validation_collection_year"] }
before do
Rake.application.rake_require("tasks/set_log_validation_collection_year")
Rake::Task.define_task(:environment)
task.reenable
end
context "when the rake task is run" do
let(:user) { create(:user) }
context "and version whodunnit exists for create" do
let!(:log_validation_2023) { LogValidation.create(from: Time.zone.local(2023, 4, 1), to: Time.zone.local(2024, 4, 1)) }
let!(:log_validation_2024) { LogValidation.create(from: Time.zone.local(2024, 4, 1), to: Time.zone.local(2025, 4, 1)) }
it "sets collection_year" do
task.invoke
expect(log_validation_2023.reload.collection_year).to eq("2023/2024")
expect(log_validation_2024.reload.collection_year).to eq("2024/2025")
end
end
end
end
end

18
spec/models/form/lettings/questions/sheltered_spec.rb

@ -25,12 +25,18 @@ RSpec.describe Form::Lettings::Questions::Sheltered, type: :model do
expect(question.type).to eq "radio" expect(question.type).to eq "radio"
end end
context "with 2023/24 form" do context "with 2024/25 form" do
before do
allow(form).to receive(:start_year_2024_or_later?).and_return(true)
allow(form).to receive(:start_year_2025_or_later?).and_return(false)
end
it "has the correct answer_options" do it "has the correct answer_options" do
expect(question.answer_options).to eq({ expect(question.answer_options).to eq({
"2" => { "value" => "Yes – extra care housing" },
"1" => { "value" => "Yes – specialist retirement housing" }, "1" => { "value" => "Yes – specialist retirement housing" },
"2" => { "value" => "Yes – extra care housing" },
"5" => { "value" => "Yes – sheltered housing for adults aged under 55 years" }, "5" => { "value" => "Yes – sheltered housing for adults aged under 55 years" },
"6" => { "value" => "Yes – sheltered housing for adults aged 55 years and over who are not retired" },
"3" => { "value" => "No" }, "3" => { "value" => "No" },
"divider" => { "value" => true }, "divider" => { "value" => true },
"4" => { "value" => "Don’t know" }, "4" => { "value" => "Don’t know" },
@ -38,17 +44,17 @@ RSpec.describe Form::Lettings::Questions::Sheltered, type: :model do
end end
end end
context "with 2024/25 form" do context "with 2025/26 form" do
before do before do
allow(form).to receive(:start_year_2024_or_later?).and_return(true) allow(form).to receive(:start_year_2024_or_later?).and_return(true)
allow(form).to receive(:start_year_2025_or_later?).and_return(true)
end end
it "has the correct answer_options" do it "has the correct answer_options" do
expect(question.answer_options).to eq({ expect(question.answer_options).to eq({
"1" => { "value" => "Yes – specialist retirement housing" }, "1" => { "value" => "Yes – sheltered housing for tenants with low support needs" },
"2" => { "value" => "Yes – extra care housing" }, "2" => { "value" => "Yes – extra care housing" },
"5" => { "value" => "Yes – sheltered housing for adults aged under 55 years" }, "7" => { "value" => "Yes - other" },
"6" => { "value" => "Yes – sheltered housing for adults aged 55 years and over who are not retired" },
"3" => { "value" => "No" }, "3" => { "value" => "No" },
"divider" => { "value" => true }, "divider" => { "value" => true },
"4" => { "value" => "Don’t know" }, "4" => { "value" => "Don’t know" },

47
spec/models/form/lettings/subsections/property_information_spec.rb

@ -14,12 +14,18 @@ RSpec.describe Form::Lettings::Subsections::PropertyInformation, type: :model do
let(:form) { instance_double(Form, start_date:) } let(:form) { instance_double(Form, start_date:) }
before do before do
allow(form).to receive(:start_year_2024_or_later?).and_return(false) allow(form).to receive(:start_year_2024_or_later?).and_return(true)
allow(form).to receive(:start_year_2025_or_later?).and_return(false)
end end
context "when 2023" do context "when 2023" do
let(:start_date) { Time.utc(2023, 2, 8) } let(:start_date) { Time.utc(2023, 2, 8) }
before do
allow(form).to receive(:start_year_2024_or_later?).and_return(false)
allow(form).to receive(:start_year_2025_or_later?).and_return(false)
end
it "has correct pages" do it "has correct pages" do
expect(property_information.pages.map(&:id)).to eq( expect(property_information.pages.map(&:id)).to eq(
%w[ %w[
@ -52,6 +58,44 @@ RSpec.describe Form::Lettings::Subsections::PropertyInformation, type: :model do
before do before do
allow(form).to receive(:start_year_2024_or_later?).and_return(true) allow(form).to receive(:start_year_2024_or_later?).and_return(true)
allow(form).to receive(:start_year_2025_or_later?).and_return(false)
end
it "has correct pages" do
expect(property_information.pages.map(&:id)).to eq(
%w[
uprn
uprn_confirmation
address_matcher
no_address_found
uprn_selection
address
property_local_authority
local_authority_rent_value_check
first_time_property_let_as_social_housing
property_let_type
property_vacancy_reason_not_first_let
property_vacancy_reason_first_let
property_unit_type
property_building_type
property_wheelchair_accessible
property_number_of_bedrooms
beds_rent_value_check
void_date
void_date_value_check
property_major_repairs
property_major_repairs_value_check
],
)
end
end
context "when 2025" do
let(:start_date) { Time.utc(2025, 2, 8) }
before do
allow(form).to receive(:start_year_2024_or_later?).and_return(true)
allow(form).to receive(:start_year_2025_or_later?).and_return(true)
end end
it "has correct pages" do it "has correct pages" do
@ -78,6 +122,7 @@ RSpec.describe Form::Lettings::Subsections::PropertyInformation, type: :model do
void_date_value_check void_date_value_check
property_major_repairs property_major_repairs
property_major_repairs_value_check property_major_repairs_value_check
sheltered_accommodation
], ],
) )
end end

24
spec/models/form/lettings/subsections/tenancy_information_spec.rb

@ -16,12 +16,18 @@ RSpec.describe Form::Lettings::Subsections::TenancyInformation, type: :model do
let(:form) { instance_double(Form, start_date:) } let(:form) { instance_double(Form, start_date:) }
before do before do
allow(form).to receive(:start_year_2024_or_later?).and_return(false) allow(form).to receive(:start_year_2024_or_later?).and_return(true)
allow(form).to receive(:start_year_2025_or_later?).and_return(false)
end end
context "when 2023" do context "when 2023" do
let(:start_date) { Time.utc(2023, 2, 8) } let(:start_date) { Time.utc(2023, 2, 8) }
before do
allow(form).to receive(:start_year_2024_or_later?).and_return(false)
allow(form).to receive(:start_year_2025_or_later?).and_return(false)
end
it "has correct pages" do it "has correct pages" do
expect(tenancy_information.pages.map(&:id)).to eq( expect(tenancy_information.pages.map(&:id)).to eq(
%w[joint starter_tenancy tenancy_type starter_tenancy_type tenancy_length tenancy_length_affordable_rent tenancy_length_intermediate_rent sheltered_accommodation], %w[joint starter_tenancy tenancy_type starter_tenancy_type tenancy_length tenancy_length_affordable_rent tenancy_length_intermediate_rent sheltered_accommodation],
@ -34,6 +40,7 @@ RSpec.describe Form::Lettings::Subsections::TenancyInformation, type: :model do
before do before do
allow(form).to receive(:start_year_2024_or_later?).and_return(true) allow(form).to receive(:start_year_2024_or_later?).and_return(true)
allow(form).to receive(:start_year_2025_or_later?).and_return(false)
end end
it "has correct pages" do it "has correct pages" do
@ -42,6 +49,21 @@ RSpec.describe Form::Lettings::Subsections::TenancyInformation, type: :model do
) )
end end
end end
context "when 2025" do
let(:start_date) { Time.utc(2025, 2, 8) }
before do
allow(form).to receive(:start_year_2024_or_later?).and_return(true)
allow(form).to receive(:start_year_2025_or_later?).and_return(true)
end
it "has correct pages" do
expect(tenancy_information.pages.map(&:id)).to eq(
%w[joint starter_tenancy tenancy_type starter_tenancy_type tenancy_length tenancy_length_affordable_rent tenancy_length_intermediate_rent tenancy_length_periodic],
)
end
end
end end
it "has the correct id" do it "has the correct id" do

12
spec/services/documentation_generator_spec.rb

@ -106,8 +106,7 @@ describe DocumentationGenerator do
expect(any_validation.field).not_to be_empty expect(any_validation.field).not_to be_empty
expect(any_validation.error_message).not_to be_empty expect(any_validation.error_message).not_to be_empty
expect(any_validation.case).to eq("Provided values fulfill the description") expect(any_validation.case).to eq("Provided values fulfill the description")
expect(any_validation.from).not_to be_nil expect(any_validation.collection_year).not_to be_nil
expect(any_validation.to).not_to be_nil
expect(any_validation.validation_type).to eq("format") expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("soft") expect(any_validation.hard_soft).to eq("soft")
expect(any_validation.other_validated_models).to eq("User") expect(any_validation.other_validated_models).to eq("User")
@ -137,8 +136,7 @@ describe DocumentationGenerator do
expect(any_validation.field).not_to be_empty expect(any_validation.field).not_to be_empty
expect(any_validation.error_message).not_to be_empty expect(any_validation.error_message).not_to be_empty
expect(any_validation.case).to eq("Provided values fulfill the description") expect(any_validation.case).to eq("Provided values fulfill the description")
expect(any_validation.from).not_to be_nil expect(any_validation.collection_year).not_to be_nil
expect(any_validation.to).not_to be_nil
expect(any_validation.validation_type).to eq("format") expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("soft") expect(any_validation.hard_soft).to eq("soft")
expect(any_validation.other_validated_models).to eq("User") expect(any_validation.other_validated_models).to eq("User")
@ -165,8 +163,7 @@ describe DocumentationGenerator do
expect(any_validation.field).to eq("ppostcode_full") expect(any_validation.field).to eq("ppostcode_full")
expect(any_validation.error_message).to eq("Enter a valid postcode") expect(any_validation.error_message).to eq("Enter a valid postcode")
expect(any_validation.case).to eq("Previous postcode is known and current postcode is blank") expect(any_validation.case).to eq("Previous postcode is known and current postcode is blank")
expect(any_validation.from).not_to be_nil expect(any_validation.collection_year).to eq("2023/2024")
expect(any_validation.to).not_to be_nil
expect(any_validation.validation_type).to eq("format") expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("hard") expect(any_validation.hard_soft).to eq("hard")
expect(any_validation.other_validated_models).to eq("User") expect(any_validation.other_validated_models).to eq("User")
@ -218,8 +215,7 @@ describe DocumentationGenerator do
expect(any_validation.field).to eq("ppostcode_full") expect(any_validation.field).to eq("ppostcode_full")
expect(any_validation.error_message).to eq("Enter a valid postcode") expect(any_validation.error_message).to eq("Enter a valid postcode")
expect(any_validation.case).to eq("Previous postcode is known and current postcode is blank") expect(any_validation.case).to eq("Previous postcode is known and current postcode is blank")
expect(any_validation.from).not_to be_nil expect(any_validation.collection_year).to eq("2023/2024")
expect(any_validation.to).not_to be_nil
expect(any_validation.validation_type).to eq("format") expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("hard") expect(any_validation.hard_soft).to eq("hard")
expect(any_validation.other_validated_models).to eq("User") expect(any_validation.other_validated_models).to eq("User")

21
spec/services/exports/organisation_export_service_spec.rb

@ -84,6 +84,27 @@ RSpec.describe Exports::OrganisationExportService do
it "returns the list with correct archive" do it "returns the list with correct archive" do
expect(export_service.export_xml_organisations).to eq({ expected_zip_filename.gsub(".zip", "") => start_time }) expect(export_service.export_xml_organisations).to eq({ expected_zip_filename.gsub(".zip", "") => start_time })
end end
context "and the organisation is merged" do
let(:expected_content) { replace_entity_ids(organisation, xml_export_file.read) }
before do
organisation.update!(merge_date: Time.zone.yesterday)
expected_content.sub!("<active>true</active>", "<active>false</active>")
expected_content.sub!("<merge_date/>", "<merge_date>#{organisation.merge_date.iso8601}</merge_date>")
expected_content.sub!("<status>active</status>", "<status>merged</status>")
end
it "generates an XML export file with the expected content within the ZIP file" do
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content|
entry = Zip::File.open_buffer(content).find_entry(expected_data_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to eq(expected_content)
end
export_service.export_xml_organisations
end
end
end end
context "and multiple organisations are available for export" do context "and multiple organisations are available for export" do

Loading…
Cancel
Save