From 53ae216bae9f7d7be5b563fe9f02e8634f61c3a8 Mon Sep 17 00:00:00 2001
From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com>
Date: Fri, 9 Aug 2024 15:59:12 +0100
Subject: [PATCH 1/7] Revert "CLDC-3566: Clear duplicate set ids when a log
ceases to be a duplicate through normal update flow (#2534)" (#2563)
This reverts commit e66d5ea1d0c380c7f7a550becec43b96518df762.
---
app/controllers/form_controller.rb | 74 +++---
app/services/feature_toggle.rb | 4 +
spec/requests/form_controller_spec.rb | 311 +++-----------------------
3 files changed, 78 insertions(+), 311 deletions(-)
diff --git a/app/controllers/form_controller.rb b/app/controllers/form_controller.rb
index 8ed367e93..d84f8642a 100644
--- a/app/controllers/form_controller.rb
+++ b/app/controllers/form_controller.rb
@@ -26,8 +26,6 @@ class FormController < ApplicationController
flash[:notice] = "You have successfully updated #{updated_question_string}"
end
- update_duplication_tracking
-
pages_requiring_update = pages_requiring_update(shown_page_ids_with_unanswered_questions_before_update)
redirect_to(successful_redirect_path(pages_requiring_update))
else
@@ -194,43 +192,24 @@ private
params[@log.model_name.param_key]["interruption_page_referrer_type"].presence
end
- def update_duplication_tracking
- class_name = @log.class.name.underscore
- dynamic_duplicates = current_user.send(class_name.pluralize).duplicate_logs(@log)
-
- if dynamic_duplicates.any?
- saved_duplicates = @log.duplicates
- if saved_duplicates.none? || duplicates_changed?(dynamic_duplicates, saved_duplicates)
- duplicate_set_id = dynamic_duplicates.first.duplicate_set_id || new_duplicate_set_id(@log)
- update_logs_with_duplicate_set_id(@log, dynamic_duplicates, duplicate_set_id)
- saved_duplicates.first.update!(duplicate_set_id: nil) if saved_duplicates.count == 1
- end
- else
- remove_fixed_duplicate_set_ids(@log)
- end
- end
-
def successful_redirect_path(pages_to_check)
- class_name = @log.class.name.underscore
-
- if is_referrer_type?("duplicate_logs") || is_referrer_type?("duplicate_logs_banner")
- original_log = current_user.send(class_name.pluralize).find_by(id: from_referrer_query("original_log_id"))
+ if FeatureToggle.deduplication_flow_enabled?
+ if is_referrer_type?("duplicate_logs") || is_referrer_type?("duplicate_logs_banner")
+ return correcting_duplicate_logs_redirect_path
+ end
- if original_log.present? && current_user.send(class_name.pluralize).duplicate_logs(original_log).any?
- if @log.duplicates.none?
- flash[:notice] = deduplication_success_banner
+ dynamic_duplicates = @log.lettings? ? current_user.lettings_logs.duplicate_logs(@log) : current_user.sales_logs.duplicate_logs(@log)
+ if dynamic_duplicates.any?
+ saved_duplicates = @log.duplicates
+ if saved_duplicates.none? || duplicates_changed?(dynamic_duplicates, saved_duplicates)
+ duplicate_set_id = dynamic_duplicates.first.duplicate_set_id || new_duplicate_set_id(@log)
+ update_logs_with_duplicate_set_id(@log, dynamic_duplicates, duplicate_set_id)
+ saved_duplicates.first.update!(duplicate_set_id: nil) if saved_duplicates.count == 1
end
- return send("#{class_name}_duplicate_logs_path", original_log, original_log_id: original_log.id, referrer: params[:referrer], organisation_id: params[:organisation_id])
- else
- flash[:notice] = deduplication_success_banner
- return send("#{class_name}_duplicate_logs_path", "#{class_name}_id".to_sym => from_referrer_query("first_remaining_duplicate_id"), original_log_id: from_referrer_query("original_log_id"), referrer: params[:referrer], organisation_id: params[:organisation_id])
+ return send("#{@log.class.name.underscore}_duplicate_logs_path", @log, original_log_id: @log.id)
end
end
- if @log.duplicates.any?
- return send("#{@log.class.name.underscore}_duplicate_logs_path", @log, original_log_id: @log.id)
- end
-
if is_referrer_type?("check_answers")
next_page_id = form.next_page_id(@page, @log, current_user)
next_page = form.get_page(next_page_id)
@@ -336,6 +315,35 @@ private
CONFIRMATION_PAGE_IDS = %w[uprn_confirmation uprn_selection].freeze
+ def correcting_duplicate_logs_redirect_path
+ class_name = @log.class.name.underscore
+
+ original_log = current_user.send(class_name.pluralize).find_by(id: from_referrer_query("original_log_id"))
+ dynamic_duplicates = current_user.send(class_name.pluralize).duplicate_logs(@log)
+
+ if dynamic_duplicates.any?
+ saved_duplicates = @log.duplicates
+ if duplicates_changed?(dynamic_duplicates, saved_duplicates)
+ duplicate_set_id = dynamic_duplicates.first.duplicate_set_id || new_duplicate_set_id(@log)
+ update_logs_with_duplicate_set_id(@log, dynamic_duplicates, duplicate_set_id)
+ saved_duplicates.first.update!(duplicate_set_id: nil) if saved_duplicates.count == 1
+ end
+ else
+ remove_fixed_duplicate_set_ids(@log)
+ end
+
+ if original_log.present? && current_user.send(class_name.pluralize).duplicate_logs(original_log).any?
+ if dynamic_duplicates.none?
+ flash[:notice] = deduplication_success_banner
+ end
+ send("#{class_name}_duplicate_logs_path", original_log, original_log_id: original_log.id, referrer: params[:referrer], organisation_id: params[:organisation_id])
+ else
+ remove_fixed_duplicate_set_ids(original_log)
+ flash[:notice] = deduplication_success_banner
+ send("#{class_name}_duplicate_logs_path", "#{class_name}_id".to_sym => from_referrer_query("first_remaining_duplicate_id"), original_log_id: from_referrer_query("original_log_id"), referrer: params[:referrer], organisation_id: params[:organisation_id])
+ end
+ end
+
def deduplication_success_banner
deduplicated_log_link = "Log #{@log.id}"
changed_labels = {
diff --git a/app/services/feature_toggle.rb b/app/services/feature_toggle.rb
index 91989ff86..810bfb845 100644
--- a/app/services/feature_toggle.rb
+++ b/app/services/feature_toggle.rb
@@ -11,6 +11,10 @@ class FeatureToggle
!Rails.env.development?
end
+ def self.deduplication_flow_enabled?
+ true
+ end
+
def self.duplicate_summary_enabled?
true
end
diff --git a/spec/requests/form_controller_spec.rb b/spec/requests/form_controller_spec.rb
index 80157e992..020dba3a4 100644
--- a/spec/requests/form_controller_spec.rb
+++ b/spec/requests/form_controller_spec.rb
@@ -763,162 +763,39 @@ RSpec.describe FormController, type: :request do
}
end
- context "when the log will not be a duplicate" do
- before do
- post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params:
- end
-
- it "re-renders the same page with errors if validation fails" do
- expect(response).to have_http_status(:redirect)
- end
-
- it "only updates answers that apply to the page being submitted" do
- lettings_log.reload
- expect(lettings_log.age1).to eq(answer)
- expect(lettings_log.age2).to be nil
- end
-
- it "tracks who updated the record" do
- lettings_log.reload
- whodunnit_actor = lettings_log.versions.last.actor
- expect(whodunnit_actor).to be_a(User)
- expect(whodunnit_actor.id).to eq(user.id)
- end
+ before do
+ post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params:
end
- context "when the answer makes the log a duplicate" do
- context "with one other log" do
- let(:new_duplicate) { create(:lettings_log) }
-
- before do
- allow(LettingsLog).to receive(:duplicate_logs).and_return(LettingsLog.where(id: new_duplicate.id))
- post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params:
- end
-
- it "sets a new duplicate set id on both logs" do
- lettings_log.reload
- new_duplicate.reload
- expect(lettings_log.duplicate_set_id).not_to be_nil
- expect(lettings_log.duplicate_set_id).to eql(new_duplicate.duplicate_set_id)
- end
-
- it "redirects to the duplicate logs page" do
- expect(response).to redirect_to("/lettings-logs/#{lettings_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}")
- follow_redirect!
- expect(page).to have_content("These logs are duplicates")
- end
- end
-
- context "with a set of other logs" do
- let(:duplicate_set_id) { 100 }
- let(:new_duplicates) { create_list(:lettings_log, 2, duplicate_set_id:) }
-
- before do
- allow(LettingsLog).to receive(:duplicate_logs).and_return(LettingsLog.where(id: new_duplicates.pluck(:id)))
- post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params:
- end
-
- it "sets the logs duplicate set id to that of the set it is now part of" do
- lettings_log.reload
- expect(lettings_log.duplicate_set_id).to eql(duplicate_set_id)
- new_duplicates.each do |log|
- log.reload
- expect(log.duplicate_set_id).to eql(duplicate_set_id)
- end
- end
-
- it "redirects to the duplicate logs page" do
- expect(response).to redirect_to("/lettings-logs/#{lettings_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}")
- follow_redirect!
- expect(page).to have_content("These logs are duplicates")
- end
- end
-
- context "when the log was previously in a different duplicate set" do
- context "with a single other log" do
- let(:old_duplicate_set_id) { 110 }
- let!(:old_duplicate) { create(:lettings_log, duplicate_set_id: old_duplicate_set_id) }
- let(:lettings_log) { create(:lettings_log, assigned_to: user, duplicate_set_id: old_duplicate_set_id) }
- let(:new_duplicate) { create(:lettings_log) }
-
- before do
- allow(LettingsLog).to receive(:duplicate_logs).and_return(LettingsLog.where(id: new_duplicate.id))
- post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params:
- end
-
- it "updates the relevant duplicate set ids" do
- lettings_log.reload
- old_duplicate.reload
- new_duplicate.reload
- expect(old_duplicate.duplicate_set_id).to be_nil
- expect(lettings_log.duplicate_set_id).not_to be_nil
- expect(lettings_log.duplicate_set_id).to eql(new_duplicate.duplicate_set_id)
- end
- end
+ it "re-renders the same page with errors if validation fails" do
+ expect(response).to have_http_status(:redirect)
+ end
- context "with multiple other logs" do
- let(:old_duplicate_set_id) { 120 }
- let!(:old_duplicates) { create_list(:lettings_log, 2, duplicate_set_id: old_duplicate_set_id) }
- let(:lettings_log) { create(:lettings_log, assigned_to: user, duplicate_set_id: old_duplicate_set_id) }
- let(:new_duplicate) { create(:lettings_log) }
-
- before do
- allow(LettingsLog).to receive(:duplicate_logs).and_return(LettingsLog.where(id: new_duplicate.id))
- post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params:
- end
-
- it "updates the relevant duplicate set ids" do
- lettings_log.reload
- new_duplicate.reload
- old_duplicates.each do |log|
- log.reload
- expect(log.duplicate_set_id).to eql(old_duplicate_set_id)
- end
- expect(lettings_log.duplicate_set_id).not_to be_nil
- expect(lettings_log.duplicate_set_id).not_to eql(old_duplicate_set_id)
- expect(lettings_log.duplicate_set_id).to eql(new_duplicate.duplicate_set_id)
- end
- end
- end
+ it "only updates answers that apply to the page being submitted" do
+ lettings_log.reload
+ expect(lettings_log.age1).to eq(answer)
+ expect(lettings_log.age2).to be nil
end
- context "when the answer makes the log stop being a duplicate" do
- context "when the log had one duplicate" do
- let(:old_duplicate_set_id) { 130 }
- let!(:old_duplicate) { create(:lettings_log, duplicate_set_id: old_duplicate_set_id) }
- let(:lettings_log) { create(:lettings_log, assigned_to: user, duplicate_set_id: old_duplicate_set_id) }
+ it "tracks who updated the record" do
+ lettings_log.reload
+ whodunnit_actor = lettings_log.versions.last.actor
+ expect(whodunnit_actor).to be_a(User)
+ expect(whodunnit_actor.id).to eq(user.id)
+ end
- before do
- allow(LettingsLog).to receive(:duplicate_logs).and_return(LettingsLog.none)
- post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params:
- end
+ context "and duplicate logs" do
+ let(:duplicate_logs) { create_list(:lettings_log, 2) }
- it "updates the relevant duplicate set ids" do
- lettings_log.reload
- old_duplicate.reload
- expect(old_duplicate.duplicate_set_id).to be_nil
- expect(lettings_log.duplicate_set_id).to be_nil
- end
+ before do
+ allow(LettingsLog).to receive(:duplicate_logs).and_return(duplicate_logs)
+ post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params:
end
- context "when the log had multiple duplicates" do
- let(:old_duplicate_set_id) { 140 }
- let!(:old_duplicates) { create_list(:lettings_log, 2, duplicate_set_id: old_duplicate_set_id) }
- let(:lettings_log) { create(:lettings_log, assigned_to: user, duplicate_set_id: old_duplicate_set_id) }
-
- before do
- allow(LettingsLog).to receive(:duplicate_logs).and_return(LettingsLog.none)
- post "/lettings-logs/#{lettings_log.id}/#{page_id.dasherize}", params:
- end
-
- it "updates the relevant duplicate set ids" do
- lettings_log.reload
- old_duplicates.each do |log|
- log.reload
- expect(log.duplicate_set_id).to eql(old_duplicate_set_id)
- end
- expect(lettings_log.duplicate_set_id).to be_nil
- end
+ it "redirects to the duplicate logs page" do
+ expect(response).to redirect_to("/lettings-logs/#{lettings_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}")
+ follow_redirect!
+ expect(page).to have_content("These logs are duplicates")
end
end
end
@@ -939,141 +816,19 @@ RSpec.describe FormController, type: :request do
},
}
end
- let(:page_id) { "buyer_1_age" }
-
- context "when the answer makes the log a duplicate" do
- context "with one other log" do
- let(:new_duplicate) { create(:sales_log) }
-
- before do
- allow(SalesLog).to receive(:duplicate_logs).and_return(SalesLog.where(id: new_duplicate.id))
- post "/sales-logs/#{sales_log.id}/#{page_id.dasherize}", params:
- end
-
- it "sets a new duplicate set id on both logs" do
- sales_log.reload
- new_duplicate.reload
- expect(sales_log.duplicate_set_id).not_to be_nil
- expect(sales_log.duplicate_set_id).to eql(new_duplicate.duplicate_set_id)
- end
-
- it "redirects to the duplicate logs page" do
- expect(response).to redirect_to("/sales-logs/#{sales_log.id}/duplicate-logs?original_log_id=#{sales_log.id}")
- follow_redirect!
- expect(page).to have_content("These logs are duplicates")
- end
- end
-
- context "with a set of other logs" do
- let(:duplicate_set_id) { 100 }
- let(:new_duplicates) { create_list(:sales_log, 2, duplicate_set_id:) }
-
- before do
- allow(SalesLog).to receive(:duplicate_logs).and_return(SalesLog.where(id: new_duplicates.pluck(:id)))
- post "/sales-logs/#{sales_log.id}/#{page_id.dasherize}", params:
- end
-
- it "sets the logs duplicate set id to that of the set it is now part of" do
- sales_log.reload
- expect(sales_log.duplicate_set_id).to eql(duplicate_set_id)
- new_duplicates.each do |log|
- log.reload
- expect(log.duplicate_set_id).to eql(duplicate_set_id)
- end
- end
-
- it "redirects to the duplicate logs page" do
- expect(response).to redirect_to("/sales-logs/#{sales_log.id}/duplicate-logs?original_log_id=#{sales_log.id}")
- follow_redirect!
- expect(page).to have_content("These logs are duplicates")
- end
- end
-
- context "when the log was previously in a different duplicate set" do
- context "with a single other log" do
- let(:old_duplicate_set_id) { 110 }
- let!(:old_duplicate) { create(:sales_log, duplicate_set_id: old_duplicate_set_id) }
- let(:sales_log) { create(:sales_log, assigned_to: user, duplicate_set_id: old_duplicate_set_id) }
- let(:new_duplicate) { create(:sales_log) }
-
- before do
- allow(SalesLog).to receive(:duplicate_logs).and_return(SalesLog.where(id: new_duplicate.id))
- post "/sales-logs/#{sales_log.id}/#{page_id.dasherize}", params:
- end
-
- it "updates the relevant duplicate set ids" do
- sales_log.reload
- old_duplicate.reload
- new_duplicate.reload
- expect(old_duplicate.duplicate_set_id).to be_nil
- expect(sales_log.duplicate_set_id).not_to be_nil
- expect(sales_log.duplicate_set_id).to eql(new_duplicate.duplicate_set_id)
- end
- end
-
- context "with multiple other logs" do
- let(:old_duplicate_set_id) { 120 }
- let!(:old_duplicates) { create_list(:sales_log, 2, duplicate_set_id: old_duplicate_set_id) }
- let(:sales_log) { create(:sales_log, assigned_to: user, duplicate_set_id: old_duplicate_set_id) }
- let(:new_duplicate) { create(:sales_log) }
-
- before do
- allow(SalesLog).to receive(:duplicate_logs).and_return(SalesLog.where(id: new_duplicate.id))
- post "/sales-logs/#{sales_log.id}/#{page_id.dasherize}", params:
- end
-
- it "updates the relevant duplicate set ids" do
- sales_log.reload
- new_duplicate.reload
- old_duplicates.each do |log|
- log.reload
- expect(log.duplicate_set_id).to eql(old_duplicate_set_id)
- end
- expect(sales_log.duplicate_set_id).not_to be_nil
- expect(sales_log.duplicate_set_id).not_to eql(old_duplicate_set_id)
- expect(sales_log.duplicate_set_id).to eql(new_duplicate.duplicate_set_id)
- end
- end
- end
- end
-
- context "when the answer makes the log stop being a duplicate" do
- context "when the log had one duplicate" do
- let(:old_duplicate_set_id) { 130 }
- let!(:old_duplicate) { create(:sales_log, duplicate_set_id: old_duplicate_set_id) }
- let(:sales_log) { create(:sales_log, assigned_to: user, duplicate_set_id: old_duplicate_set_id) }
- before do
- allow(SalesLog).to receive(:duplicate_logs).and_return(SalesLog.none)
- post "/sales-logs/#{sales_log.id}/#{page_id.dasherize}", params:
- end
+ context "and duplicate logs" do
+ let!(:duplicate_logs) { create_list(:sales_log, 2) }
- it "updates the relevant duplicate set ids" do
- sales_log.reload
- old_duplicate.reload
- expect(old_duplicate.duplicate_set_id).to be_nil
- expect(sales_log.duplicate_set_id).to be_nil
- end
+ before do
+ allow(SalesLog).to receive(:duplicate_logs).and_return(duplicate_logs)
+ post "/sales-logs/#{sales_log.id}/buyer-1-age", params:
end
- context "when the log had multiple duplicates" do
- let(:old_duplicate_set_id) { 140 }
- let!(:old_duplicates) { create_list(:sales_log, 2, duplicate_set_id: old_duplicate_set_id) }
- let(:sales_log) { create(:sales_log, assigned_to: user, duplicate_set_id: old_duplicate_set_id) }
-
- before do
- allow(SalesLog).to receive(:duplicate_logs).and_return(SalesLog.none)
- post "/sales-logs/#{sales_log.id}/#{page_id.dasherize}", params:
- end
-
- it "updates the relevant duplicate set ids" do
- sales_log.reload
- old_duplicates.each do |log|
- log.reload
- expect(log.duplicate_set_id).to eql(old_duplicate_set_id)
- end
- expect(sales_log.duplicate_set_id).to be_nil
- end
+ it "redirects to the duplicate logs page" do
+ expect(response).to redirect_to("/sales-logs/#{sales_log.id}/duplicate-logs?original_log_id=#{sales_log.id}")
+ follow_redirect!
+ expect(page).to have_content("These logs are duplicates")
end
end
end
From 72890e516e6888c8a0253d4c1c51e2b6cc33717f Mon Sep 17 00:00:00 2001
From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com>
Date: Tue, 13 Aug 2024 12:01:37 +0100
Subject: [PATCH 2/7] CLDC-3564 Update filter search (#2535)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Add search controller
* Update on confirm
* lint
* Update search endpoint
* remove assigned to filter options
* Explicitly define host in the url
* Update search controller
* Allow searching organisations
* Allow filtering by user without js
* Allow filtering by org without js
* Add filter_type to method calls
* Update filter helpers
* Hide text search input when js is enabled
* Some feature test updates
* fix model test
* Delete a random file 👀
* lint
* Update more tests
* Update inner text for filter
* Keep csv filters the same
* Clear free text filters for csv dowloads
* User path helper
* Update scheme filters and log scopes
* Update which users we can filter by
---
app/controllers/organisations_controller.rb | 15 +-
app/controllers/users_controller.rb | 11 +
app/frontend/controllers/index.js | 3 +
app/frontend/controllers/search_controller.js | 46 +++
app/frontend/modules/search.js | 49 +++
app/helpers/filters_helper.rb | 82 ++++-
app/models/lettings_log.rb | 4 +
app/models/log.rb | 3 +
app/models/organisation.rb | 1 +
app/models/user.rb | 5 +-
app/services/filter_manager.rb | 11 +
app/views/filters/_radio_filter.html.erb | 1 +
.../filters/_text_select_filter.html.erb | 20 ++
app/views/filters/assigned_to.html.erb | 3 +-
app/views/filters/managed_by.html.erb | 3 +-
app/views/filters/owned_by.html.erb | 3 +-
app/views/logs/_log_filters.html.erb | 15 +-
app/views/schemes/_scheme_filters.html.erb | 2 +-
config/routes.rb | 8 +
spec/features/lettings_log_spec.rb | 4 +-
spec/features/organisation_spec.rb | 6 +-
spec/features/user_spec.rb | 7 +-
spec/helpers/filters_helper_spec.rb | 298 ++++++++++++++++--
spec/models/user_spec.rb | 6 +-
.../requests/organisations_controller_spec.rb | 45 +++
spec/requests/users_controller_spec.rb | 61 ++++
26 files changed, 643 insertions(+), 69 deletions(-)
create mode 100644 app/frontend/controllers/search_controller.js
create mode 100644 app/views/filters/_text_select_filter.html.erb
diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb
index dfe50fa22..9d3e63b33 100644
--- a/app/controllers/organisations_controller.rb
+++ b/app/controllers/organisations_controller.rb
@@ -4,8 +4,8 @@ class OrganisationsController < ApplicationController
include DuplicateLogsHelper
before_action :authenticate_user!
- before_action :find_resource, except: %i[index new create]
- before_action :authenticate_scope!, except: [:index]
+ before_action :find_resource, except: %i[index new create search]
+ before_action :authenticate_scope!, except: %i[index search]
before_action :session_filters, if: -> { current_user.support? || current_user.organisation.has_managing_agents? }, only: %i[lettings_logs sales_logs email_lettings_csv download_lettings_csv email_sales_csv download_sales_csv]
before_action :session_filters, only: %i[users schemes email_schemes_csv download_schemes_csv]
before_action -> { filter_manager.serialize_filters_to_session }, if: -> { current_user.support? || current_user.organisation.has_managing_agents? }, only: %i[lettings_logs sales_logs email_lettings_csv download_lettings_csv email_sales_csv download_sales_csv]
@@ -280,6 +280,17 @@ class OrganisationsController < ApplicationController
render "schemes/changes"
end
+ def search
+ org_options = current_user.support? ? Organisation.all : Organisation.affiliated_organisations(current_user.organisation)
+ organisations = org_options.search_by(params["query"]).limit(20)
+
+ org_data = organisations.each_with_object({}) do |org, hash|
+ hash[org.id] = { value: org.name }
+ end
+
+ render json: org_data.to_json
+ end
+
private
def filter_type
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 3fe3b1813..2f7bb2bd6 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -32,6 +32,17 @@ class UsersController < ApplicationController
end
end
+ def search
+ user_options = current_user.support? ? User.all : User.own_and_managing_org_users(current_user.organisation)
+ users = user_options.search_by(params["query"]).limit(20)
+
+ user_data = users.each_with_object({}) do |user, hash|
+ hash[user.id] = { value: user.name, hint: user.email }
+ end
+
+ render json: user_data.to_json
+ end
+
def resend_invite
@user.send_confirmation_instructions
flash[:notice] = "Invitation sent to #{@user.email}"
diff --git a/app/frontend/controllers/index.js b/app/frontend/controllers/index.js
index ece539d15..ef29b99ca 100644
--- a/app/frontend/controllers/index.js
+++ b/app/frontend/controllers/index.js
@@ -13,6 +13,8 @@ import GovukfrontendController from './govukfrontend_controller.js'
import NumericQuestionController from './numeric_question_controller.js'
+import SearchController from './search_controller.js'
+
import FilterLayoutController from './filter_layout_controller.js'
application.register('accessible-autocomplete', AccessibleAutocompleteController)
application.register('conditional-filter', ConditionalFilterController)
@@ -20,3 +22,4 @@ application.register('conditional-question', ConditionalQuestionController)
application.register('govukfrontend', GovukfrontendController)
application.register('numeric-question', NumericQuestionController)
application.register('filter-layout', FilterLayoutController)
+application.register('search', SearchController)
diff --git a/app/frontend/controllers/search_controller.js b/app/frontend/controllers/search_controller.js
new file mode 100644
index 000000000..565b9d0d1
--- /dev/null
+++ b/app/frontend/controllers/search_controller.js
@@ -0,0 +1,46 @@
+import { Controller } from '@hotwired/stimulus'
+import accessibleAutocomplete from 'accessible-autocomplete'
+import 'accessible-autocomplete/dist/accessible-autocomplete.min.css'
+import { searchSuggestion, fetchAndPopulateSearchResults, confirmSelectedOption, searchableName } from '../modules/search'
+
+const options = []
+const populateOptions = (results, selectEl) => {
+ selectEl.innerHTML = ''
+
+ Object.keys(results).forEach((key) => {
+ const option = document.createElement('option')
+ option.value = key
+ option.innerHTML = results[key].value
+ if (results[key].hint) { option.setAttribute('data-hint', results[key].hint) }
+ option.setAttribute('text', searchableName(results[key]))
+ selectEl.appendChild(option)
+ options.push(option)
+ })
+}
+
+export default class extends Controller {
+ connect () {
+ const selectEl = this.element
+ const matches = /^(\w+)\[(\w+)\]$/.exec(selectEl.name)
+ const rawFieldName = matches ? `${matches[1]}[${matches[2]}_raw]` : ''
+ const searchUrl = JSON.parse(this.element.dataset.info).search_url
+
+ document.querySelectorAll('.non-js-text-search-input-field').forEach((el) => {
+ el.style.display = 'none'
+ })
+
+ accessibleAutocomplete.enhanceSelectElement({
+ defaultValue: '',
+ selectElement: selectEl,
+ minLength: 1,
+ source: (query, populateResults) => {
+ fetchAndPopulateSearchResults(query, populateResults, searchUrl, populateOptions, selectEl)
+ },
+ autoselect: true,
+ placeholder: 'Start typing to search',
+ templates: { suggestion: (value) => searchSuggestion(value, options) },
+ name: rawFieldName,
+ onConfirm: (val) => confirmSelectedOption(selectEl, val)
+ })
+ }
+}
diff --git a/app/frontend/modules/search.js b/app/frontend/modules/search.js
index 71944746e..efdf7b9d0 100644
--- a/app/frontend/modules/search.js
+++ b/app/frontend/modules/search.js
@@ -117,6 +117,22 @@ export const suggestion = (value, options) => {
}
}
+export const searchSuggestion = (value, options) => {
+ try {
+ const option = options.find((o) => o.getAttribute('text') === value)
+ if (option) {
+ const result = enhanceOption(option)
+ const html = result.append ? `${result.text} ${result.append}` : `${result.text}`
+ return result.hint ? `${html}
${result.hint}
` : html
+ } else {
+ return 'No results found'
+ }
+ } catch (error) {
+ console.error('Error fetching user option:', error)
+ return value
+ }
+}
+
export const enhanceOption = (option) => {
return {
text: option.text,
@@ -128,6 +144,39 @@ export const enhanceOption = (option) => {
}
}
+export const fetchAndPopulateSearchResults = async (query, populateResults, relativeUrlRoute, populateOptions, selectEl) => {
+ if (/\S/.test(query)) {
+ const results = await fetchUserOptions(query, relativeUrlRoute)
+ populateOptions(results, selectEl)
+ populateResults(Object.values(results).map((o) => searchableName(o)))
+ }
+}
+
+export const fetchUserOptions = async (query, searchUrl) => {
+ try {
+ const response = await fetch(`${searchUrl}?query=${encodeURIComponent(query)}`)
+ const results = await response.json()
+ return results
+ } catch (error) {
+ console.error('Error fetching user options:', error)
+ return []
+ }
+}
+
export const getSearchableName = (option) => {
return option.getAttribute('data-hint') ? option.text + ' ' + option.getAttribute('data-hint') : option.text
}
+
+export const searchableName = (option) => {
+ return option.hint ? option.value + ' ' + option.hint : option.value
+}
+
+export const confirmSelectedOption = (selectEl, val) => {
+ const arrayOfOptions = Array.from(selectEl.options).filter(function (option, index, arr) { return option.value !== '' })
+
+ const selectedOption = [].filter.call(
+ arrayOfOptions,
+ (option) => option.getAttribute('text') === val
+ )[0]
+ if (selectedOption) selectedOption.selected = true
+}
diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb
index 38c15b82b..5f8488bc9 100644
--- a/app/helpers/filters_helper.rb
+++ b/app/helpers/filters_helper.rb
@@ -11,8 +11,8 @@ module FiltersHelper
return true if !selected_filters.key?("owning_organisation") && filter == "owning_organisation_select" && value == :all
return true if !selected_filters.key?("managing_organisation") && filter == "managing_organisation_select" && value == :all
- return true if selected_filters["owning_organisation"].present? && filter == "owning_organisation_select" && value == :specific_org
- return true if selected_filters["managing_organisation"].present? && filter == "managing_organisation_select" && value == :specific_org
+ return true if (selected_filters["owning_organisation"].present? || selected_filters["owning_organisation_text_search"].present?) && filter == "owning_organisation_select" && value == :specific_org
+ return true if (selected_filters["managing_organisation"].present? || selected_filters["managing_organisation_text_search"].present?) && filter == "managing_organisation_select" && value == :specific_org
return false if selected_filters[filter].blank?
@@ -84,16 +84,54 @@ module FiltersHelper
JSON.parse(session[session_name_for(filter_type)])[filter] || ""
end
- def owning_organisation_filter_options(user)
+ def all_owning_organisation_filter_options(user)
organisation_options = user.support? ? Organisation.all : ([user.organisation] + user.organisation.stock_owners + user.organisation.absorbed_organisations).uniq
[OpenStruct.new(id: "", name: "Select an option")] + organisation_options.map { |org| OpenStruct.new(id: org.id, name: org.name) }
end
- def assigned_to_filter_options(user)
+ def owning_organisation_filter_options(user, filter_type)
+ if applied_filters(filter_type)["owning_organisation"].present?
+ organisation_id = applied_filters(filter_type)["owning_organisation"]
+
+ org = if user.support?
+ Organisation.where(id: organisation_id)&.first
+ else
+ Organisation.affiliated_organisations(user.organisation).where(id: organisation_id)&.first
+ end
+ return [OpenStruct.new(id: org.id, name: org.name)] if org.present?
+ end
+
+ [OpenStruct.new(id: "", name: "Select an option")]
+ end
+
+ def assigned_to_csv_filter_options(user)
user_options = user.support? ? User.all : (user.organisation.users + user.organisation.managing_agents.flat_map(&:users) + user.organisation.stock_owners.flat_map(&:users)).uniq
[OpenStruct.new(id: "", name: "Select an option", hint: "")] + user_options.map { |user_option| OpenStruct.new(id: user_option.id, name: user_option.name, hint: user_option.email) }
end
+ def assigned_to_filter_options(filter_type)
+ if applied_filters(filter_type)["assigned_to"] == "specific_user" && applied_filters(filter_type)["user"].present?
+ user_id = applied_filters(filter_type)["user"]
+ selected_user = if current_user.support?
+ User.where(id: user_id)&.first
+ else
+ User.own_and_managing_org_users(current_user.organisation).where(id: user_id)&.first
+ end
+
+ return [OpenStruct.new(id: selected_user.id, name: selected_user.name, hint: selected_user.email)] if selected_user.present?
+ end
+ [OpenStruct.new(id: "", name: "Select an option", hint: "")]
+ end
+
+ def filter_search_url(category)
+ case category
+ when :user
+ search_users_path
+ when :owning_organisation, :managing_organisation
+ search_organisations_path
+ end
+ end
+
def collection_year_options
years = {
current_collection_start_year.to_s => year_combo(current_collection_start_year),
@@ -125,11 +163,26 @@ module FiltersHelper
end
end
- def managing_organisation_filter_options(user)
+ def managing_organisation_csv_filter_options(user)
organisation_options = user.support? ? Organisation.all : ([user.organisation] + user.organisation.managing_agents + user.organisation.absorbed_organisations).uniq
[OpenStruct.new(id: "", name: "Select an option")] + organisation_options.map { |org| OpenStruct.new(id: org.id, name: org.name) }
end
+ def managing_organisation_filter_options(user, filter_type)
+ if applied_filters(filter_type)["managing_organisation"].present?
+ organisation_id = applied_filters(filter_type)["managing_organisation"]
+
+ org = if user.support?
+ Organisation.where(id: organisation_id)&.first
+ else
+ Organisation.affiliated_organisations(user.organisation).where(id: organisation_id)&.first
+ end
+ return [OpenStruct.new(id: org.id, name: org.name)] if org.present?
+ end
+
+ [OpenStruct.new(id: "", name: "Select an option")]
+ end
+
def show_scheme_managing_org_filter?(user)
org = user.organisation
@@ -176,8 +229,8 @@ module FiltersHelper
{ id: "status", label: "Status", value: formatted_status_filter(session_filters) },
filter_type == "lettings_logs" ? { id: "needstype", label: "Needs type", value: formatted_needstype_filter(session_filters) } : nil,
{ id: "assigned_to", label: "Assigned to", value: formatted_assigned_to_filter(session_filters) },
- { id: "owned_by", label: "Owned by", value: formatted_owned_by_filter(session_filters) },
- { id: "managed_by", label: "Managed by", value: formatted_managed_by_filter(session_filters) },
+ { id: "owned_by", label: "Owned by", value: formatted_owned_by_filter(session_filters, filter_type) },
+ { id: "managed_by", label: "Managed by", value: formatted_managed_by_filter(session_filters, filter_type) },
].compact
end
@@ -221,7 +274,7 @@ private
filters.each.sum do |category, category_filters|
if %w[years status needstypes bulk_upload_id].include?(category)
category_filters.count(&:present?)
- elsif %w[user owning_organisation managing_organisation].include?(category)
+ elsif %w[user owning_organisation managing_organisation user_text_search owning_organisation_text_search managing_organisation_text_search].include?(category)
1
else
0
@@ -256,26 +309,27 @@ private
return "All" if session_filters["assigned_to"].include?("all")
return "You" if session_filters["assigned_to"].include?("you")
- selected_user_option = assigned_to_filter_options(current_user).find { |x| x.id == session_filters["user"].to_i }
+ User.own_and_managing_org_users(current_user.organisation).find(session_filters["user"].to_i).name
+ selected_user_option = User.own_and_managing_org_users(current_user.organisation).find(session_filters["user"].to_i)
return unless selected_user_option
- "#{selected_user_option.name} (#{selected_user_option.hint})"
+ "#{selected_user_option.name} (#{selected_user_option.email})"
end
- def formatted_owned_by_filter(session_filters)
+ def formatted_owned_by_filter(session_filters, filter_type)
return "All" if params["id"].blank? && (session_filters["owning_organisation"].blank? || session_filters["owning_organisation"]&.include?("all"))
session_org_id = session_filters["owning_organisation"] || params["id"]
- selected_owning_organisation_option = owning_organisation_filter_options(current_user).find { |org| org.id == session_org_id.to_i }
+ selected_owning_organisation_option = owning_organisation_filter_options(current_user, filter_type).find { |org| org.id == session_org_id.to_i }
return unless selected_owning_organisation_option
selected_owning_organisation_option&.name
end
- def formatted_managed_by_filter(session_filters)
+ def formatted_managed_by_filter(session_filters, filter_type)
return "All" if session_filters["managing_organisation"].blank? || session_filters["managing_organisation"].include?("all")
- selected_managing_organisation_option = managing_organisation_filter_options(current_user).find { |org| org.id == session_filters["managing_organisation"].to_i }
+ selected_managing_organisation_option = managing_organisation_filter_options(current_user, filter_type).find { |org| org.id == session_filters["managing_organisation"].to_i }
return unless selected_managing_organisation_option
selected_managing_organisation_option&.name
diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb
index e7a72350d..10ab612cd 100644
--- a/app/models/lettings_log.rb
+++ b/app/models/lettings_log.rb
@@ -132,6 +132,10 @@ class LettingsLog < Log
illness_type_10: false)
}
+ scope :filter_by_user_text_search, ->(param, user) { where(assigned_to: user.support? ? User.search_by(param) : User.own_and_managing_org_users(user.organisation).search_by(param)) }
+ scope :filter_by_owning_organisation_text_search, ->(param, _user) { where(owning_organisation: Organisation.search_by(param)) }
+ scope :filter_by_managing_organisation_text_search, ->(param, _user) { where(managing_organisation: Organisation.search_by(param)) }
+
AUTOGENERATED_FIELDS = %w[id status created_at updated_at discarded_at].freeze
OPTIONAL_FIELDS = %w[tenancycode propcode chcharge].freeze
RENT_TYPE_MAPPING_LABELS = { 1 => "Social Rent", 2 => "Affordable Rent", 3 => "Intermediate Rent" }.freeze
diff --git a/app/models/log.rb b/app/models/log.rb
index c095a8276..3a6c1e982 100644
--- a/app/models/log.rb
+++ b/app/models/log.rb
@@ -53,6 +53,9 @@ class Log < ApplicationRecord
scope :filter_by_organisation, ->(org, _user = nil) { where(owning_organisation: org).or(where(managing_organisation: org)) }
scope :filter_by_owning_organisation, ->(owning_organisation, _user = nil) { where(owning_organisation:) }
scope :filter_by_managing_organisation, ->(managing_organisation, _user = nil) { where(managing_organisation:) }
+ scope :filter_by_user_text_search, ->(param, user) { where(assigned_to: user.support? ? User.search_by(param) : User.own_and_managing_org_users(user.organisation).search_by(param)) }
+ scope :filter_by_owning_organisation_text_search, ->(param, _user) { where(owning_organisation: Organisation.search_by(param)) }
+ scope :filter_by_managing_organisation_text_search, ->(param, _user) { where(managing_organisation: Organisation.search_by(param)) }
attr_accessor :skip_update_status, :skip_update_uprn_confirmed, :select_best_address_match, :skip_dpo_validation
diff --git a/app/models/organisation.rb b/app/models/organisation.rb
index 8f77df166..65b35c24e 100644
--- a/app/models/organisation.rb
+++ b/app/models/organisation.rb
@@ -18,6 +18,7 @@ class Organisation < ApplicationRecord
belongs_to :absorbing_organisation, class_name: "Organisation", optional: true
has_many :absorbed_organisations, class_name: "Organisation", foreign_key: "absorbing_organisation_id"
scope :visible, -> { where(discarded_at: nil) }
+ scope :affiliated_organisations, ->(organisation) { where(id: (organisation.child_organisations + [organisation] + organisation.parent_organisations + organisation.absorbed_organisations).map(&:id)) }
def affiliated_stock_owners
ids = []
diff --git a/app/models/user.rb b/app/models/user.rb
index d25faaa53..c79ceb0d9 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -77,6 +77,7 @@ class User < ApplicationRecord
scope :deactivated, -> { where(active: false) }
scope :active_status, -> { where(active: true).where.not(last_sign_in_at: nil) }
scope :visible, -> { where(discarded_at: nil) }
+ scope :own_and_managing_org_users, ->(organisation) { where(organisation: organisation.child_organisations + [organisation]) }
def lettings_logs
if support?
@@ -209,9 +210,9 @@ class User < ApplicationRecord
def logs_filters(specific_org: false)
if (support? && !specific_org) || organisation.has_managing_agents? || organisation.has_stock_owners?
- %w[years status needstypes assigned_to user managing_organisation owning_organisation bulk_upload_id]
+ %w[years status needstypes assigned_to user managing_organisation owning_organisation bulk_upload_id user_text_search owning_organisation_text_search managing_organisation_text_search]
else
- %w[years status needstypes assigned_to user bulk_upload_id]
+ %w[years status needstypes assigned_to user bulk_upload_id user_text_search]
end
end
diff --git a/app/services/filter_manager.rb b/app/services/filter_manager.rb
index 8d661b3fa..9f68a097c 100644
--- a/app/services/filter_manager.rb
+++ b/app/services/filter_manager.rb
@@ -24,6 +24,9 @@ class FilterManager
next if category == "owning_organisation" && all_orgs
next if category == "managing_organisation" && all_orgs
next if category == "assigned_to"
+ next if category == "user_text_search" && filters["assigned_to"] != "specific_user"
+ next if category == "owning_organisation_text_search" && all_orgs
+ next if category == "managing_organisation_text_search" && all_orgs
logs = logs.public_send("filter_by_#{category}", values, user)
end
@@ -94,11 +97,19 @@ class FilterManager
new_filters[filter] = params[filter] if params[filter].present?
end
+ if params["action"] == "download_csv"
+ new_filters["assigned_to"] = "all" if new_filters["assigned_to"] == "specific_user" && new_filters["user_text_search"].present?
+ new_filters["owning_organisation_select"] = "all" if new_filters["owning_organisation_select"] == "specific_organisation" && new_filters["owning_organisation_text_search"].present?
+ new_filters["managing_organisation_select"] = "all" if new_filters["managing_organisation_select"] == "specific_organisation" && new_filters["managing_organisation_text_search"].present?
+ end
new_filters = new_filters.except("owning_organisation") if params["owning_organisation_select"] == "all"
new_filters = new_filters.except("managing_organisation") if params["managing_organisation_select"] == "all"
new_filters = new_filters.except("user") if params["assigned_to"] == "all"
new_filters["user"] = current_user.id.to_s if params["assigned_to"] == "you"
+ new_filters = new_filters.except("user_text_search") if params["assigned_to"] == "all" || params["assigned_to"] == "you"
+ new_filters = new_filters.except("owning_organisation_text_search") if params["owning_organisation_select"] == "all"
+ new_filters = new_filters.except("managing_organisation_text_search") if params["managing_organisation_select"] == "all"
end
if (filter_type.include?("schemes") || filter_type.include?("users") || filter_type.include?("scheme_locations")) && params["status"].present?
diff --git a/app/views/filters/_radio_filter.html.erb b/app/views/filters/_radio_filter.html.erb
index 6eb902dba..e4d23573c 100644
--- a/app/views/filters/_radio_filter.html.erb
+++ b/app/views/filters/_radio_filter.html.erb
@@ -10,6 +10,7 @@
collection: option[:conditional_filter][:options],
category: option[:conditional_filter][:category],
label: option[:conditional_filter][:label],
+ caption_text: option[:conditional_filter][:caption_text],
secondary: true,
hint_text: option[:conditional_filter][:hint_text],
} %>
diff --git a/app/views/filters/_text_select_filter.html.erb b/app/views/filters/_text_select_filter.html.erb
new file mode 100644
index 000000000..ecc997bba
--- /dev/null
+++ b/app/views/filters/_text_select_filter.html.erb
@@ -0,0 +1,20 @@
+
+<%= f.govuk_text_field "#{category}_text_search".to_sym,
+ label: { text: label, hidden: secondary },
+ "data-controller": "search conditional-filter",
+ caption: { text: caption_text },
+ "data-info": { search_url: filter_search_url(category.to_sym) }.to_json,
+ value: selected_option("#{category}_text_search", @filter_type) %>
+
+<%= f.govuk_select(category.to_sym,
+ label: { text: label, hidden: secondary },
+ "data-controller": "search conditional-filter",
+ "hidden": true,
+ "data-info": { search_url: filter_search_url(category.to_sym) }.to_json) do %>
+ <% collection.each do |answer| %>
+
+ <% end %>
+ <% end %>
diff --git a/app/views/filters/assigned_to.html.erb b/app/views/filters/assigned_to.html.erb
index 9f0582fbb..778d63c8a 100644
--- a/app/views/filters/assigned_to.html.erb
+++ b/app/views/filters/assigned_to.html.erb
@@ -11,7 +11,8 @@
type: "select",
label: "User",
category: "user",
- options: assigned_to_filter_options(current_user),
+ caption_text: "User's name or email",
+ options: assigned_to_csv_filter_options(current_user),
},
},
},
diff --git a/app/views/filters/managed_by.html.erb b/app/views/filters/managed_by.html.erb
index e3d849c9b..5d4b684f3 100644
--- a/app/views/filters/managed_by.html.erb
+++ b/app/views/filters/managed_by.html.erb
@@ -9,7 +9,8 @@
type: "select",
label: "Managed by",
category: "managing_organisation",
- options: managing_organisation_filter_options(current_user),
+ options: managing_organisation_csv_filter_options(current_user),
+ caption_text: "Organisation name",
},
},
},
diff --git a/app/views/filters/owned_by.html.erb b/app/views/filters/owned_by.html.erb
index 7acfd459c..271b68de9 100644
--- a/app/views/filters/owned_by.html.erb
+++ b/app/views/filters/owned_by.html.erb
@@ -9,7 +9,8 @@
type: "select",
label: "Owning Organisation",
category: "owning_organisation",
- options: owning_organisation_filter_options(current_user),
+ options: all_owning_organisation_filter_options(current_user),
+ caption_text: "Organisation name",
},
},
},
diff --git a/app/views/logs/_log_filters.html.erb b/app/views/logs/_log_filters.html.erb
index aaef70377..3beab4b6b 100644
--- a/app/views/logs/_log_filters.html.erb
+++ b/app/views/logs/_log_filters.html.erb
@@ -66,10 +66,11 @@
"specific_user": {
label: "Specific user",
conditional_filter: {
- type: "select",
+ type: "text_select",
label: "User",
category: "user",
- options: assigned_to_filter_options(current_user),
+ options: assigned_to_filter_options(@filter_type),
+ caption_text: "User's name or email",
},
},
},
@@ -86,10 +87,11 @@
"specific_org": {
label: "Specific owning organisation",
conditional_filter: {
- type: "select",
+ type: "text_select",
label: "Owning Organisation",
category: "owning_organisation",
- options: owning_organisation_filter_options(current_user),
+ options: owning_organisation_filter_options(current_user, @filter_type),
+ caption_text: "Organisation name",
},
},
},
@@ -107,10 +109,11 @@
"specific_org": {
label: "Specific managing organisation",
conditional_filter: {
- type: "select",
+ type: "text_select",
label: user_or_org_lettings_path? ? "Managed by" : "Reported by",
category: "managing_organisation",
- options: managing_organisation_filter_options(current_user),
+ options: managing_organisation_filter_options(current_user, @filter_type),
+ caption_text: "Organisation name",
},
},
},
diff --git a/app/views/schemes/_scheme_filters.html.erb b/app/views/schemes/_scheme_filters.html.erb
index ca0538463..51687a096 100644
--- a/app/views/schemes/_scheme_filters.html.erb
+++ b/app/views/schemes/_scheme_filters.html.erb
@@ -35,7 +35,7 @@
type: "select",
label: "Owning Organisation",
category: "owning_organisation",
- options: owning_organisation_filter_options(current_user),
+ options: all_owning_organisation_filter_options(current_user),
},
},
},
diff --git a/config/routes.rb b/config/routes.rb
index e0d9631e9..faea457fe 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -125,6 +125,10 @@ Rails.application.routes.draw do
get "edit-dpo", to: "users#dpo"
get "edit-key-contact", to: "users#key_contact"
+ collection do
+ get :search
+ end
+
member do
get "deactivate", to: "users#deactivate"
get "reactivate", to: "users#reactivate"
@@ -191,6 +195,10 @@ Rails.application.routes.draw do
get "delete-confirmation", to: "organisations#delete_confirmation"
delete "delete", to: "organisations#delete"
end
+
+ collection do
+ get :search
+ end
end
resources :merge_requests, path: "/merge-request" do
diff --git a/spec/features/lettings_log_spec.rb b/spec/features/lettings_log_spec.rb
index 2b977fdd7..ac9a1e4a8 100644
--- a/spec/features/lettings_log_spec.rb
+++ b/spec/features/lettings_log_spec.rb
@@ -89,9 +89,9 @@ RSpec.describe "Lettings Log Features" do
check("In progress")
choose("You")
choose("Specific owning organisation")
- select(stock_owner_1.name, from: "owning_organisation")
+ fill_in("owning-organisation-text-search-field", with: "stock")
choose("Specific managing organisation")
- select(managing_agent_1.name, from: "managing_organisation")
+ fill_in("managing-organisation-text-search-field", with: "managing")
click_button("Apply filters")
end
diff --git a/spec/features/organisation_spec.rb b/spec/features/organisation_spec.rb
index 65f787c2a..1867285eb 100644
--- a/spec/features/organisation_spec.rb
+++ b/spec/features/organisation_spec.rb
@@ -199,14 +199,14 @@ RSpec.describe "User Features" do
it "can filter lettings logs by year" do
check("years-2022-field")
click_button("Apply filters")
- expect(page).to have_current_path("/organisations/#{org_id}/lettings-logs?years[]=&years[]=2022&status[]=&needstypes[]=&assigned_to=all&user=&owning_organisation_select=all&owning_organisation=&managing_organisation_select=all&managing_organisation=")
+ expect(page).to have_current_path("/organisations/#{org_id}/lettings-logs?years[]=&years[]=2022&status[]=&needstypes[]=&assigned_to=all&user_text_search=&user=&owning_organisation_select=all&owning_organisation_text_search=&owning_organisation=&managing_organisation_select=all&managing_organisation_text_search=&managing_organisation=")
expect(page).not_to have_link first_log.id.to_s, href: "/lettings-logs/#{first_log.id}"
end
it "can filter lettings logs by needstype" do
check("needstypes-1-field")
click_button("Apply filters")
- expect(page).to have_current_path("/organisations/#{org_id}/lettings-logs?years[]=&status[]=&needstypes[]=&needstypes[]=1&assigned_to=all&user=&owning_organisation_select=all&owning_organisation=&managing_organisation_select=all&managing_organisation=")
+ expect(page).to have_current_path("/organisations/#{org_id}/lettings-logs?years[]=&status[]=&needstypes[]=&needstypes[]=1&assigned_to=all&user_text_search=&user=&owning_organisation_select=all&owning_organisation_text_search=&owning_organisation=&managing_organisation_select=all&managing_organisation_text_search=&managing_organisation=")
other_general_needs_logs.each do |general_needs_log|
expect(page).to have_link general_needs_log.id.to_s, href: "/lettings-logs/#{general_needs_log.id}"
end
@@ -245,7 +245,7 @@ RSpec.describe "User Features" do
end
check("years-2022-field")
click_button("Apply filters")
- expect(page).to have_current_path("/organisations/#{org_id}/sales-logs?years[]=&years[]=2022&status[]=&assigned_to=all&user=&owning_organisation_select=all&owning_organisation=&managing_organisation_select=all&managing_organisation=")
+ expect(page).to have_current_path("/organisations/#{org_id}/sales-logs?years[]=&years[]=2022&status[]=&assigned_to=all&user_text_search=&user=&owning_organisation_select=all&owning_organisation_text_search=&owning_organisation=&managing_organisation_select=all&managing_organisation_text_search=&managing_organisation=")
expect(page).not_to have_link first_log.id.to_s, href: "/sales-logs/#{first_log.id}"
end
end
diff --git a/spec/features/user_spec.rb b/spec/features/user_spec.rb
index 169465cb1..c30abe1e9 100644
--- a/spec/features/user_spec.rb
+++ b/spec/features/user_spec.rb
@@ -796,12 +796,13 @@ RSpec.describe "User Features" do
visit("/lettings-logs")
choose("owning-organisation-select-specific-org-field", allow_label_click: true)
expect(page).to have_field("owning-organisation-field", with: "")
- find("#owning-organisation-field").click.native.send_keys("F", "i", "l", "t", :down, :enter)
+ find("#owning-organisation-field").click.native.send_keys("F", "i", "l", "t")
+ select(parent_organisation.name, from: "owning-organisation-field-select", visible: false)
click_button("Apply filters")
- expect(page).to have_current_path("/lettings-logs?%5Byears%5D%5B%5D=&%5Bstatus%5D%5B%5D=&%5Bneedstypes%5D%5B%5D=&assigned_to=all&owning_organisation_select=specific_org&owning_organisation=#{parent_organisation.id}&managing_organisation_select=all")
+ expect(page).to have_current_path("/lettings-logs?%5Byears%5D%5B%5D=&%5Bstatus%5D%5B%5D=&%5Bneedstypes%5D%5B%5D=&assigned_to=all&user_text_search=&owning_organisation_select=specific_org&owning_organisation_text_search=&owning_organisation=#{parent_organisation.id}&managing_organisation_select=all&managing_organisation_text_search=")
choose("owning-organisation-select-all-field", allow_label_click: true)
click_button("Apply filters")
- expect(page).to have_current_path("/lettings-logs?%5Byears%5D%5B%5D=&%5Bstatus%5D%5B%5D=&%5Bneedstypes%5D%5B%5D=&assigned_to=all&owning_organisation_select=all&managing_organisation_select=all")
+ expect(page).to have_current_path("/lettings-logs?%5Byears%5D%5B%5D=&%5Bstatus%5D%5B%5D=&%5Bneedstypes%5D%5B%5D=&assigned_to=all&user_text_search=&owning_organisation_select=all&owning_organisation_text_search=&managing_organisation_select=all&managing_organisation_text_search=")
end
end
end
diff --git a/spec/helpers/filters_helper_spec.rb b/spec/helpers/filters_helper_spec.rb
index f04157521..c57f92311 100644
--- a/spec/helpers/filters_helper_spec.rb
+++ b/spec/helpers/filters_helper_spec.rb
@@ -175,27 +175,146 @@ RSpec.describe FiltersHelper do
context "with a support user" do
let(:user) { FactoryBot.create(:user, :support, organisation: child_organisation) }
- it "returns a list of all organisations" do
- expect(owning_organisation_filter_options(user)).to match_array([
- OpenStruct.new(id: "", name: "Select an option"),
- OpenStruct.new(id: child_organisation.id, name: "Child organisation"),
- OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"),
- OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"),
- OpenStruct.new(id: 99, name: "Other organisation"),
- ])
+ context "when no organisation is selected in the filters" do
+ it "returns an empty list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: "", name: "Select an option"),
+ ])
+ end
+ end
+
+ context "when a specific child organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "owning_organisation": child_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: child_organisation.id, name: "Child organisation"),
+ ])
+ end
+ end
+
+ context "when a specific parent organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "owning_organisation": parent_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"),
+ ])
+ end
+ end
+
+ context "when a specific absorbed organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "owning_organisation": absorbed_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"),
+ ])
+ end
+ end
+
+ context "when a specific non related organisation is selected in the filters" do
+ let(:unrelated_organisation) { create(:organisation, name: "Unrelated organisation") }
+
+ before do
+ session[:lettings_logs_filters] = { "owning_organisation": unrelated_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: unrelated_organisation.id, name: "Unrelated organisation"),
+ ])
+ end
+ end
+
+ context "when a non existing organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "owning_organisation": 143_542_542 }.to_json
+ end
+
+ it "returns an empty list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: "", name: "Select an option"),
+ ])
+ end
end
end
context "with a data coordinator user" do
let(:user) { FactoryBot.create(:user, :data_coordinator, organisation: child_organisation) }
- it "returns a list of parent orgs and your own organisation" do
- expect(owning_organisation_filter_options(user.reload)).to eq([
- OpenStruct.new(id: "", name: "Select an option"),
- OpenStruct.new(id: child_organisation.id, name: "Child organisation"),
- OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"),
- OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"),
- ])
+ context "when no organisation is selected in the filters" do
+ it "returns an empty list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: "", name: "Select an option"),
+ ])
+ end
+ end
+
+ context "when a specific child organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "owning_organisation": child_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: child_organisation.id, name: "Child organisation"),
+ ])
+ end
+ end
+
+ context "when a specific parent organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "owning_organisation": parent_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"),
+ ])
+ end
+ end
+
+ context "when a specific absorbed organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "owning_organisation": absorbed_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"),
+ ])
+ end
+ end
+
+ context "when a specific non related organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "owning_organisation": create(:organisation).id }.to_json
+ end
+
+ it "returns an empty list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: "", name: "Select an option"),
+ ])
+ end
+ end
+
+ context "when a non existing organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "owning_organisation": 143_542_542 }.to_json
+ end
+
+ it "returns an empty list" do
+ expect(owning_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: "", name: "Select an option"),
+ ])
+ end
end
end
end
@@ -214,27 +333,146 @@ RSpec.describe FiltersHelper do
context "with a support user" do
let(:user) { FactoryBot.create(:user, :support, organisation: parent_organisation) }
- it "returns a list of all organisations" do
- expect(managing_organisation_filter_options(user)).to eq([
- OpenStruct.new(id: "", name: "Select an option"),
- OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"),
- OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"),
- OpenStruct.new(id: child_organisation.id, name: "Child organisation"),
- OpenStruct.new(id: 99, name: "Other organisation"),
- ])
+ context "when no organisation is selected in the filters" do
+ it "returns an empty list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: "", name: "Select an option"),
+ ])
+ end
+ end
+
+ context "when a specific child organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "managing_organisation": child_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: child_organisation.id, name: "Child organisation"),
+ ])
+ end
+ end
+
+ context "when a specific parent organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "managing_organisation": parent_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"),
+ ])
+ end
+ end
+
+ context "when a specific absorbed organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "managing_organisation": absorbed_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"),
+ ])
+ end
+ end
+
+ context "when a specific non related organisation is selected in the filters" do
+ let(:unrelated_organisation) { create(:organisation, name: "Unrelated organisation") }
+
+ before do
+ session[:lettings_logs_filters] = { "managing_organisation": unrelated_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: unrelated_organisation.id, name: "Unrelated organisation"),
+ ])
+ end
+ end
+
+ context "when a non existing organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "managing_organisation": 143_542_542 }.to_json
+ end
+
+ it "returns an empty list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: "", name: "Select an option"),
+ ])
+ end
end
end
context "with a data coordinator user" do
let(:user) { FactoryBot.create(:user, :data_coordinator, organisation: parent_organisation) }
- it "returns a list of child orgs and your own organisation" do
- expect(managing_organisation_filter_options(user.reload)).to eq([
- OpenStruct.new(id: "", name: "Select an option"),
- OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"),
- OpenStruct.new(id: child_organisation.id, name: "Child organisation"),
- OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"),
- ])
+ context "when no organisation is selected in the filters" do
+ it "returns an empty list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: "", name: "Select an option"),
+ ])
+ end
+ end
+
+ context "when a specific child organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "managing_organisation": child_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: child_organisation.id, name: "Child organisation"),
+ ])
+ end
+ end
+
+ context "when a specific parent organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "managing_organisation": parent_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: parent_organisation.id, name: "Parent organisation"),
+ ])
+ end
+ end
+
+ context "when a specific absorbed organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "managing_organisation": absorbed_organisation.id }.to_json
+ end
+
+ it "returns the selected organisation in the list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: absorbed_organisation.id, name: "Absorbed organisation"),
+ ])
+ end
+ end
+
+ context "when a specific non related organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "managing_organisation": create(:organisation).id }.to_json
+ end
+
+ it "returns an empty list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: "", name: "Select an option"),
+ ])
+ end
+ end
+
+ context "when a non existing organisation is selected in the filters" do
+ before do
+ session[:lettings_logs_filters] = { "managing_organisation": 143_542_542 }.to_json
+ end
+
+ it "returns an empty list" do
+ expect(managing_organisation_filter_options(user.reload, "lettings_logs")).to eq([
+ OpenStruct.new(id: "", name: "Select an option"),
+ ])
+ end
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index edb998ac3..6a04e9a0b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -164,7 +164,7 @@ RSpec.describe User, type: :model do
end
it "can filter lettings logs by user, year and status" do
- expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user bulk_upload_id])
+ expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user bulk_upload_id user_text_search])
end
end
@@ -174,7 +174,7 @@ RSpec.describe User, type: :model do
end
it "can filter lettings logs by user, year, status, managing_organisation and owning_organisation" do
- expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user managing_organisation owning_organisation bulk_upload_id])
+ expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user managing_organisation owning_organisation bulk_upload_id managing_organisation_text_search owning_organisation_text_search user_text_search])
end
end
end
@@ -215,7 +215,7 @@ RSpec.describe User, type: :model do
end
it "can filter lettings logs by user, year, status, managing_organisation and owning_organisation" do
- expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user owning_organisation managing_organisation bulk_upload_id])
+ expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user owning_organisation managing_organisation bulk_upload_id managing_organisation_text_search owning_organisation_text_search user_text_search])
end
end
diff --git a/spec/requests/organisations_controller_spec.rb b/spec/requests/organisations_controller_spec.rb
index 13879a38c..d3e8a8155 100644
--- a/spec/requests/organisations_controller_spec.rb
+++ b/spec/requests/organisations_controller_spec.rb
@@ -75,6 +75,13 @@ RSpec.describe OrganisationsController, type: :request do
end
end
end
+
+ describe "#search" do
+ it "redirects to the sign in page" do
+ get "/organisations/search"
+ expect(response).to redirect_to("/account/sign-in")
+ end
+ end
end
context "when user is signed in" do
@@ -807,6 +814,25 @@ RSpec.describe OrganisationsController, type: :request do
end
end
end
+
+ describe "#search" do
+ let(:parent_organisation) { create(:organisation, name: "parent test organisation") }
+ let(:child_organisation) { create(:organisation, name: "child test organisation") }
+
+ before do
+ user.organisation.update!(name: "test organisation")
+ create(:organisation_relationship, parent_organisation: user.organisation, child_organisation:)
+ create(:organisation_relationship, child_organisation: user.organisation, parent_organisation:)
+ create(:organisation, name: "other organisation test organisation")
+ end
+
+ it "only searches within the current user's organisation, managing agents and stock owners" do
+ get "/organisations/search", headers:, params: { query: "test organisation" }
+ result = JSON.parse(response.body)
+ expect(result.count).to eq(3)
+ expect(result.keys).to match_array([user.organisation.id.to_s, parent_organisation.id.to_s, child_organisation.id.to_s])
+ end
+ end
end
context "with a data provider user" do
@@ -2077,6 +2103,25 @@ RSpec.describe OrganisationsController, type: :request do
end
end
end
+
+ describe "#search" do
+ let(:parent_organisation) { create(:organisation, name: "parent test organisation") }
+ let(:child_organisation) { create(:organisation, name: "child test organisation") }
+ let!(:other_organisation) { create(:organisation, name: "other organisation test organisation") }
+
+ before do
+ user.organisation.update!(name: "test organisation")
+ create(:organisation_relationship, parent_organisation: user.organisation, child_organisation:)
+ create(:organisation_relationship, child_organisation: user.organisation, parent_organisation:)
+ end
+
+ it "searches within all the organisations" do
+ get "/organisations/search", headers:, params: { query: "test organisation" }
+ result = JSON.parse(response.body)
+ expect(result.count).to eq(4)
+ expect(result.keys).to match_array([user.organisation.id.to_s, parent_organisation.id.to_s, child_organisation.id.to_s, other_organisation.id.to_s])
+ end
+ end
end
end
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index bb0a1cca3..8e87f7f28 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -117,6 +117,13 @@ RSpec.describe UsersController, type: :request do
expect(response).to redirect_to("/account/sign-in")
end
end
+
+ describe "#search" do
+ it "redirects to the sign in page" do
+ get "/users/search"
+ expect(response).to redirect_to("/account/sign-in")
+ end
+ end
end
context "when user is signed in as a data provider" do
@@ -404,6 +411,25 @@ RSpec.describe UsersController, type: :request do
expect(response).to have_http_status(:unauthorized)
end
end
+
+ describe "#search" do
+ let(:parent_relationship) { create(:organisation_relationship, parent_organisation: user.organisation) }
+ let(:child_relationship) { create(:organisation_relationship, child_organisation: user.organisation) }
+ let!(:org_user) { create(:user, organisation: user.organisation, name: "test_name") }
+ let!(:managing_user) { create(:user, organisation: parent_relationship.child_organisation, name: "managing_agent_test_name") }
+
+ before do
+ create(:user, organisation: child_relationship.parent_organisation, name: "stock_owner_test_name")
+ create(:user, name: "other_organisation_test_name")
+ end
+
+ it "only searches within the current user's organisation and managing agents" do
+ get "/users/search", headers:, params: { query: "test_name" }
+ result = JSON.parse(response.body)
+ expect(result.count).to eq(2)
+ expect(result.keys).to match_array([org_user.id.to_s, managing_user.id.to_s])
+ end
+ end
end
context "when user is signed in as a data coordinator" do
@@ -1174,6 +1200,25 @@ RSpec.describe UsersController, type: :request do
expect(response).to have_http_status(:unauthorized)
end
end
+
+ describe "#search" do
+ let(:parent_relationship) { create(:organisation_relationship, parent_organisation: user.organisation) }
+ let(:child_relationship) { create(:organisation_relationship, child_organisation: user.organisation) }
+ let!(:org_user) { create(:user, organisation: user.organisation, email: "test_name@example.com") }
+ let!(:managing_user) { create(:user, organisation: parent_relationship.child_organisation, email: "managing_agent_test_name@example.com") }
+
+ before do
+ create(:user, email: "other_organisation_test_name@example.com")
+ create(:user, organisation: child_relationship.parent_organisation, email: "stock_owner_test_name@example.com")
+ end
+
+ it "only searches within the current user's organisation and managing agents" do
+ get "/users/search", headers:, params: { query: "test_name" }
+ result = JSON.parse(response.body)
+ expect(result.count).to eq(2)
+ expect(result.keys).to match_array([org_user.id.to_s, managing_user.id.to_s])
+ end
+ end
end
context "when user is signed in as a support user" do
@@ -2111,6 +2156,22 @@ RSpec.describe UsersController, type: :request do
expect(page).not_to have_link("User to be deleted")
end
end
+
+ describe "#search" do
+ let(:parent_relationship) { create(:organisation_relationship, parent_organisation: user.organisation) }
+ let(:child_relationship) { create(:organisation_relationship, child_organisation: user.organisation) }
+ let!(:org_user) { create(:user, organisation: user.organisation, name: "test_name") }
+ let!(:managing_user) { create(:user, organisation: child_relationship.parent_organisation, name: "stock_owner_test_name") }
+ let!(:owner_user) { create(:user, organisation: parent_relationship.child_organisation, name: "managing_agent_test_name") }
+ let!(:other_user) { create(:user, name: "other_organisation_test_name") }
+
+ it "searches all users" do
+ get "/users/search", headers:, params: { query: "test_name" }
+ result = JSON.parse(response.body)
+ expect(result.count).to eq(4)
+ expect(result.keys).to match_array([org_user.id.to_s, managing_user.id.to_s, owner_user.id.to_s, other_user.id.to_s])
+ end
+ end
end
describe "title link" do
From 85bbbe61088229d5d3fd70f423d42c8f10367f55 Mon Sep 17 00:00:00 2001
From: Rachael Booth
Date: Tue, 13 Aug 2024 15:11:40 +0100
Subject: [PATCH 3/7] CLDC-3556: Ignore address data in bulk upload for
supported housing logs (#2540)
* Only set address data in BU for general needs logs
* Don't route to uprn selection page for supported housing logs
* Fix tests
---
.../form/lettings/pages/uprn_selection.rb | 2 +-
.../lettings/year2024/row_parser.rb | 43 +++++++++---------
.../lettings/year2024/row_parser_spec.rb | 44 +++++++++++++++----
3 files changed, 59 insertions(+), 30 deletions(-)
diff --git a/app/models/form/lettings/pages/uprn_selection.rb b/app/models/form/lettings/pages/uprn_selection.rb
index 162c608f3..4c7ca4ae1 100644
--- a/app/models/form/lettings/pages/uprn_selection.rb
+++ b/app/models/form/lettings/pages/uprn_selection.rb
@@ -17,7 +17,7 @@ class Form::Lettings::Pages::UprnSelection < ::Form::Page
end
def routed_to?(log, _current_user = nil)
- (log.uprn_known.nil? || log.uprn_known.zero?) && log.address_line1_input.present? && log.postcode_full_input.present? && (1..10).cover?(log.address_options&.count)
+ !log.is_supported_housing? && (log.uprn_known.nil? || log.uprn_known.zero?) && log.address_line1_input.present? && log.postcode_full_input.present? && (1..10).cover?(log.address_options&.count)
end
def skip_text
diff --git a/app/services/bulk_upload/lettings/year2024/row_parser.rb b/app/services/bulk_upload/lettings/year2024/row_parser.rb
index e523c11c4..913e5d9e5 100644
--- a/app/services/bulk_upload/lettings/year2024/row_parser.rb
+++ b/app/services/bulk_upload/lettings/year2024/row_parser.rb
@@ -1130,10 +1130,6 @@ private
attributes["lettype"] = nil # should get this from rent_type
attributes["tenancycode"] = field_13
- attributes["la"] = field_23
- attributes["la_as_entered"] = field_23
- attributes["postcode_known"] = postcode_known
- attributes["postcode_full"] = postcode_full
attributes["owning_organisation"] = owning_organisation
attributes["managing_organisation"] = managing_organisation
attributes["renewal"] = renewal
@@ -1304,22 +1300,29 @@ private
attributes["first_time_property_let_as_social_housing"] = first_time_property_let_as_social_housing
- attributes["uprn_known"] = field_16.present? ? 1 : 0
- attributes["uprn_confirmed"] = 1 if field_16.present?
- attributes["skip_update_uprn_confirmed"] = true
- attributes["uprn"] = field_16
- attributes["address_line1"] = field_17
- attributes["address_line1_as_entered"] = field_17
- attributes["address_line2"] = field_18
- attributes["address_line2_as_entered"] = field_18
- attributes["town_or_city"] = field_19
- attributes["town_or_city_as_entered"] = field_19
- attributes["county"] = field_20
- attributes["county_as_entered"] = field_20
- attributes["address_line1_input"] = address_line1_input
- attributes["postcode_full_input"] = postcode_full
- attributes["postcode_full_as_entered"] = postcode_full
- attributes["select_best_address_match"] = true if field_16.blank? && !supported_housing?
+ if general_needs?
+ attributes["uprn_known"] = field_16.present? ? 1 : 0
+ attributes["uprn_confirmed"] = 1 if field_16.present?
+ attributes["skip_update_uprn_confirmed"] = true
+ attributes["uprn"] = field_16
+ attributes["address_line1"] = field_17
+ attributes["address_line1_as_entered"] = field_17
+ attributes["address_line2"] = field_18
+ attributes["address_line2_as_entered"] = field_18
+ attributes["town_or_city"] = field_19
+ attributes["town_or_city_as_entered"] = field_19
+ attributes["county"] = field_20
+ attributes["county_as_entered"] = field_20
+ attributes["postcode_full"] = postcode_full
+ attributes["postcode_full_as_entered"] = postcode_full
+ attributes["postcode_known"] = postcode_known
+ attributes["la"] = field_23
+ attributes["la_as_entered"] = field_23
+
+ attributes["address_line1_input"] = address_line1_input
+ attributes["postcode_full_input"] = postcode_full
+ attributes["select_best_address_match"] = true if field_16.blank?
+ end
attributes
end
diff --git a/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb b/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb
index 25529cc19..42a05e33c 100644
--- a/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb
+++ b/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb
@@ -1867,7 +1867,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do
end
describe "#uprn" do
- let(:attributes) { { bulk_upload:, field_16: "12" } }
+ let(:attributes) { { bulk_upload:, field_4: 1, field_16: "12" } }
it "sets to given value" do
expect(parser.log.uprn).to eql("12")
@@ -1876,7 +1876,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do
describe "#uprn_known" do
context "when uprn specified" do
- let(:attributes) { { bulk_upload:, field_16: "12" } }
+ let(:attributes) { { bulk_upload:, field_4: 1, field_16: "12" } }
it "sets to 1" do
expect(parser.log.uprn_known).to be(1)
@@ -1885,7 +1885,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do
end
context "when uprn blank" do
- let(:attributes) { { bulk_upload:, field_16: "", field_4: 1 } }
+ let(:attributes) { { bulk_upload:, field_4: 1, field_16: "" } }
it "sets to 0" do
expect(parser.log.uprn_known).to be(0)
@@ -1894,7 +1894,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do
end
describe "#address_line1" do
- let(:attributes) { { bulk_upload:, field_17: "123 Sesame Street" } }
+ let(:attributes) { { bulk_upload:, field_4: 1, field_17: "123 Sesame Street" } }
it "sets to given value" do
expect(parser.log.address_line1).to eql("123 Sesame Street")
@@ -1902,7 +1902,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do
end
describe "#address_line2" do
- let(:attributes) { { bulk_upload:, field_18: "Cookie Town" } }
+ let(:attributes) { { bulk_upload:, field_4: 1, field_18: "Cookie Town" } }
it "sets to given value" do
expect(parser.log.address_line2).to eql("Cookie Town")
@@ -1910,7 +1910,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do
end
describe "#town_or_city" do
- let(:attributes) { { bulk_upload:, field_19: "London" } }
+ let(:attributes) { { bulk_upload:, field_4: 1, field_19: "London" } }
it "sets to given value" do
expect(parser.log.town_or_city).to eql("London")
@@ -1918,13 +1918,39 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do
end
describe "#county" do
- let(:attributes) { { bulk_upload:, field_20: "Greater London" } }
+ let(:attributes) { { bulk_upload:, field_4: 1, field_20: "Greater London" } }
it "sets to given value" do
expect(parser.log.county).to eql("Greater London")
end
end
+ describe "address related fields for supported housing logs" do
+ context "when address data is provided for a supported housing log" do
+ let(:attributes) { { bulk_upload:, field_4: 2, field_16: nil, field_17: "Flat 1", field_18: "Example Place", field_19: "London", field_20: "Greater London", field_21: "SW1A", field_22: "1AA" } }
+
+ it "is not set on the log" do
+ expect(parser.log.uprn).to be_nil
+ expect(parser.log.uprn_known).to be_nil
+ expect(parser.log.address_line1).to be_nil
+ expect(parser.log.address_line1_as_entered).to be_nil
+ expect(parser.log.address_line2).to be_nil
+ expect(parser.log.address_line2_as_entered).to be_nil
+ expect(parser.log.town_or_city).to be_nil
+ expect(parser.log.town_or_city_as_entered).to be_nil
+ expect(parser.log.county).to be_nil
+ expect(parser.log.county_as_entered).to be_nil
+ expect(parser.log.postcode_full).to be_nil
+ expect(parser.log.postcode_full_as_entered).to be_nil
+ expect(parser.log.la).to be_nil
+ expect(parser.log.la_as_entered).to be_nil
+ expect(parser.log.address_line1_input).to be_nil
+ expect(parser.log.postcode_full_input).to be_nil
+ expect(parser.log.select_best_address_match).to be_nil
+ end
+ end
+ end
+
[
%w[age1_known details_known_1 age1 field_42 field_47 field_49],
%w[age2_known details_known_2 age2 field_48 field_47 field_49],
@@ -2611,7 +2637,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do
end
describe "#postcode_full" do
- let(:attributes) { { bulk_upload:, field_21: " EC1N ", field_22: " 2TD " } }
+ let(:attributes) { { bulk_upload:, field_4: 1, field_21: " EC1N ", field_22: " 2TD " } }
it "strips whitespace" do
expect(parser.log.postcode_full).to eql("EC1N 2TD")
@@ -2619,7 +2645,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do
end
describe "#la" do
- let(:attributes) { { bulk_upload:, field_23: "E07000223" } }
+ let(:attributes) { { bulk_upload:, field_4: 1, field_23: "E07000223" } }
it "sets to given value" do
expect(parser.log.la).to eql("E07000223")
From bee90d6f9964c7e981baea80b1e07ce52f71ea31 Mon Sep 17 00:00:00 2001
From: Manny Dinssa <44172848+Dinssa@users.noreply.github.com>
Date: Wed, 14 Aug 2024 16:34:30 +0100
Subject: [PATCH 4/7] CLDC-2588++update soft validation errors in bulk upload
(#2577)
* Change table column width with govuk class
* Add html_safe to error message fields
---
app/components/bulk_upload_error_row_component.html.erb | 8 ++++----
.../bulk_upload_error_summary_table_component.html.erb | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/components/bulk_upload_error_row_component.html.erb b/app/components/bulk_upload_error_row_component.html.erb
index 4e9ff1390..9447c9d37 100644
--- a/app/components/bulk_upload_error_row_component.html.erb
+++ b/app/components/bulk_upload_error_row_component.html.erb
@@ -24,8 +24,8 @@
<% critical_errors.each do |error| %>
<% body.with_row do |row| %>
<% row.with_cell(text: error.cell) %>
- <% row.with_cell(text: question_for_field(error.field)) %>
- <% row.with_cell(text: error.error, html_attributes: { class: "govuk-!-font-weight-bold" }) %>
+ <% row.with_cell(text: question_for_field(error.field), html_attributes: { class: "govuk-!-width-one-half" }) %>
+ <% row.with_cell(text: error.error.html_safe, html_attributes: { class: "govuk-!-font-weight-bold govuk-!-width-one-half" }) %>
<% row.with_cell(text: error.field.humanize) %>
<% end %>
<% end %>
@@ -54,9 +54,9 @@
<% row_class += " last-row" if index == errors.size - 1 %>
<% body.with_row(html_attributes: { class: row_class }) do |row| %>
<% row.with_cell(text: error.cell) %>
- <% row.with_cell(text: question_for_field(error.field)) %>
+ <% row.with_cell(text: question_for_field(error.field), html_attributes: { class: "govuk-!-width-one-half" }) %>
<% if index == 0 %>
- <% row.with_cell(text: error_message, rowspan: errors.size, html_attributes: { class: "govuk-!-font-weight-bold grouped-multirow-cell" }) %>
+ <% row.with_cell(text: error_message.html_safe, rowspan: errors.size, html_attributes: { class: "govuk-!-font-weight-bold govuk-!-width-one-half grouped-multirow-cell" }) %>
<% end %>
<% row.with_cell(text: error.field.humanize) %>
<% 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 c0e10cffa..f9b42f34d 100644
--- a/app/components/bulk_upload_error_summary_table_component.html.erb
+++ b/app/components/bulk_upload_error_summary_table_component.html.erb
@@ -12,7 +12,7 @@
<%= table.with_body do |body| %>
<% body.with_row do |row| %>
- <% row.with_cell(text: error[0][2]) %>
+ <% row.with_cell(text: error[0][2].html_safe) %>
<% row.with_cell(text: pluralize(error[1], "error"), numeric: true) %>
<% end %>
<% end %>
From 331ee8f0e85fb6365ecbd361c2861daca82475a3 Mon Sep 17 00:00:00 2001
From: Manny Dinssa <44172848+Dinssa@users.noreply.github.com>
Date: Fri, 16 Aug 2024 09:21:03 +0100
Subject: [PATCH 5/7] CLDC-3575: Remove reference to dluhc on GitHub (#2581)
* Update github README.md picture
* Change name
* Remove old image & change private to public beta text
---
README.md | 4 ++--
.../lettings/year2023/row_parser.rb | 2 +-
docs/images/service.jpeg | Bin 0 -> 1661758 bytes
docs/images/service.png | Bin 414003 -> 0 bytes
4 files changed, 3 insertions(+), 3 deletions(-)
create mode 100644 docs/images/service.jpeg
delete mode 100644 docs/images/service.png
diff --git a/README.md b/README.md
index 1e5bb10f8..61e9d028e 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](https://github.com/communitiesuk/submit-social-housing-lettings-and-sales-data/actions/workflows/production_pipeline.yml)
[](https://github.com/communitiesuk/submit-social-housing-lettings-and-sales-data/actions/workflows/staging_pipeline.yml)
-Ruby on Rails app that handles the submission of lettings and sales of social housing data in England. Currently in private beta.
+Ruby on Rails app that handles the submission of lettings and sales of social housing data in England. Currently in public beta.
## Domain documentation
@@ -15,4 +15,4 @@ Ruby on Rails app that handles the submission of lettings and sales of social ho
## User interface
-
+
diff --git a/app/services/bulk_upload/lettings/year2023/row_parser.rb b/app/services/bulk_upload/lettings/year2023/row_parser.rb
index 1684d22ab..a09dc2bfe 100644
--- a/app/services/bulk_upload/lettings/year2023/row_parser.rb
+++ b/app/services/bulk_upload/lettings/year2023/row_parser.rb
@@ -48,7 +48,7 @@ class BulkUpload::Lettings::Year2023::RowParser
field_42: "If 'Other', what is the type of tenancy?",
field_43: "What is the length of the fixed-term tenancy to the nearest year?",
field_44: "Is this letting sheltered accommodation?",
- field_45: "Has tenant seen the DLUHC privacy notice?",
+ field_45: "Has tenant seen the MHCLG privacy notice?",
field_46: "What is the lead tenant's age?",
field_47: "Which of these best describes the lead tenant's gender identity?",
field_48: "Which of these best describes the lead tenant's ethnic background?",
diff --git a/docs/images/service.jpeg b/docs/images/service.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..6e01c95a5746b450b6910a6a0b900125df6b0da7
GIT binary patch
literal 1661758
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 uywYeNHP!mg+AO@?H4fmOP7n>{rk`5^Mj73
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