Browse Source

Merge branch 'main' into CLDC-2670-redirect-to-follow-up-questions

pull/2362/head
natdeanlewissoftwire 2 years ago
parent
commit
eba6d14d03
  1. 4
      app/controllers/organisations_controller.rb
  2. 13
      app/controllers/users_controller.rb
  3. 2
      app/helpers/tag_helper.rb
  4. 4
      app/helpers/user_helper.rb
  5. 6
      app/models/form/lettings/questions/created_by_id.rb
  6. 123
      app/models/form/lettings/questions/referral.rb
  7. 141
      app/models/form/lettings/questions/referral_prp.rb
  8. 132
      app/models/form/lettings/questions/referral_supported_housing.rb
  9. 112
      app/models/form/lettings/questions/referral_supported_housing_prp.rb
  10. 4
      app/models/form/sales/questions/created_by_id.rb
  11. 6
      app/models/lettings_log.rb
  12. 5
      app/models/sales_log.rb
  13. 17
      app/models/user.rb
  14. 25
      app/policies/user_policy.rb
  15. 6
      app/services/feature_toggle.rb
  16. 6
      app/views/users/_user_list.html.erb
  17. 24
      app/views/users/delete_confirmation.html.erb
  18. 18
      app/views/users/show.html.erb
  19. 1
      config/locales/en.yml
  20. 2
      config/routes.rb
  21. 5
      db/migrate/20240305091927_add_discarded_at_column_to_users.rb
  22. 1
      db/schema.rb
  23. 8
      spec/features/user_spec.rb
  24. 21
      spec/models/form/lettings/questions/created_by_id_spec.rb
  25. 101
      spec/models/form/lettings/questions/referral_spec.rb
  26. 20
      spec/models/form/sales/questions/created_by_id_spec.rb
  27. 24
      spec/models/lettings_log_spec.rb
  28. 94
      spec/models/sales_log_spec.rb
  29. 9
      spec/models/user_spec.rb
  30. 124
      spec/policies/user_policy_spec.rb
  31. 260
      spec/requests/users_controller_spec.rb
  32. 54
      yarn.lock

4
app/controllers/organisations_controller.rb

@ -46,14 +46,14 @@ class OrganisationsController < ApplicationController
end
def users
organisation_users = @organisation.users.sorted_by_organisation_and_role
organisation_users = @organisation.users.visible.sorted_by_organisation_and_role
unpaginated_filtered_users = filter_manager.filtered_users(organisation_users, search_term, session_filters)
respond_to do |format|
format.html do
@pagy, @users = pagy(unpaginated_filtered_users)
@searched = search_term.presence
@total_count = @organisation.users.size
@total_count = @organisation.users.visible.size
@filter_type = "users"
if current_user.support?

13
app/controllers/users_controller.rb

@ -13,7 +13,7 @@ class UsersController < ApplicationController
def index
redirect_to users_organisation_path(current_user.organisation) unless current_user.support?
all_users = User.sorted_by_organisation_and_role
all_users = User.visible.sorted_by_organisation_and_role
filtered_users = filter_manager.filtered_users(all_users, search_term, session_filters)
@pagy, @users = pagy(filtered_users)
@searched = search_term.presence
@ -122,6 +122,16 @@ class UsersController < ApplicationController
end
end
def delete_confirmation
authorize @user
end
def delete
authorize @user
@user.discard!
redirect_to users_organisation_path(@user.organisation), notice: I18n.t("notification.user_deleted", name: @user.name)
end
private
def validate_attributes
@ -197,6 +207,7 @@ private
if action_name == "create"
head :unauthorized and return unless current_user.data_coordinator? || current_user.support?
else
render_not_found and return if @user.status == :deleted
render_not_found and return unless (current_user.organisation == @user.organisation) || current_user.support?
render_not_found and return if action_name == "edit_password" && current_user != @user
render_not_found and return unless action_name == "show" ||

2
app/helpers/tag_helper.rb

@ -30,7 +30,7 @@ module TagHelper
deactivated: "grey",
deleted: "red",
merged: "orange",
unconfirmed: "red",
unconfirmed: "blue",
}.freeze
def status_tag(status, classes = [])

4
app/helpers/user_helper.rb

@ -10,4 +10,8 @@ module UserHelper
def can_edit_org?(current_user)
current_user.data_coordinator? || current_user.support?
end
def delete_user_link(user)
govuk_button_link_to "Delete this user", delete_confirmation_user_path(user), warning: true
end
end

6
app/models/form/lettings/questions/created_by_id.rb

@ -22,15 +22,15 @@ class Form::Lettings::Questions::CreatedById < ::Form::Question
[
(
if log.owning_organisation
log.owning_organisation.absorbing_organisation.present? ? log.owning_organisation&.absorbing_organisation&.users : log.owning_organisation&.users
log.owning_organisation.absorbing_organisation.present? ? log.owning_organisation&.absorbing_organisation&.users&.visible : log.owning_organisation&.users&.visible
end),
(
if log.managing_organisation
log.managing_organisation.absorbing_organisation.present? ? log.managing_organisation&.absorbing_organisation&.users : log.managing_organisation.users
log.managing_organisation.absorbing_organisation.present? ? log.managing_organisation&.absorbing_organisation&.users&.visible : log.managing_organisation.users&.visible
end),
].flatten
else
current_user.organisation.users
current_user.organisation.users.visible
end.uniq.compact
users.each_with_object(ANSWER_OPTS.dup) do |user, hsh|

123
app/models/form/lettings/questions/referral.rb

@ -7,49 +7,92 @@ class Form::Lettings::Questions::Referral < ::Form::Question
@type = "radio"
@check_answers_card_number = 0
@hint_text = "You told us that you are a local authority and that the needs type is general needs. We have removed some options because of this."
@answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
ANSWER_OPTIONS = {
"1" => {
"value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
},
"2" => {
"value" => "Tenant applied directly (no referral or nomination)",
},
"8" => {
"value" => "Re-located through official housing mobility scheme",
},
"10" => {
"value" => "Other social landlord",
},
"9" => {
"value" => "Community learning disability team",
},
"14" => {
"value" => "Community mental health team",
},
"15" => {
"value" => "Health service",
},
"12" => {
"value" => "Police, probation or prison",
},
"7" => {
"value" => "Voluntary agency",
},
"13" => {
"value" => "Youth offending team",
},
"17" => {
"value" => "Children’s Social Care",
},
"16" => {
"value" => "Other",
},
}.freeze
def answer_options
if form.start_year_after_2024?
{
"1" => {
"value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
},
"2" => {
"value" => "Tenant applied directly (no referral or nomination)",
},
"8" => {
"value" => "Re-located through official housing mobility scheme",
},
"10" => {
"value" => "Other social landlord",
},
"9" => {
"value" => "Community learning disability team",
},
"14" => {
"value" => "Community mental health team",
},
"15" => {
"value" => "Health service",
},
"18" => {
"value" => "Police, probation, prison or youth offending team – tenant had custodial sentence",
},
"19" => {
"value" => "Police, probation, prison or youth offending team – no custodial sentence",
},
"7" => {
"value" => "Voluntary agency",
},
"17" => {
"value" => "Children’s Social Care",
},
"16" => {
"value" => "Other",
},
}.freeze
else
{
"1" => {
"value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
},
"2" => {
"value" => "Tenant applied directly (no referral or nomination)",
},
"8" => {
"value" => "Re-located through official housing mobility scheme",
},
"10" => {
"value" => "Other social landlord",
},
"9" => {
"value" => "Community learning disability team",
},
"14" => {
"value" => "Community mental health team",
},
"15" => {
"value" => "Health service",
},
"12" => {
"value" => "Police, probation or prison",
},
"7" => {
"value" => "Voluntary agency",
},
"13" => {
"value" => "Youth offending team",
},
"17" => {
"value" => "Children’s Social Care",
},
"16" => {
"value" => "Other",
},
}.freeze
end
end
QUESTION_NUMBER_FROM_YEAR = { 2023 => 85, 2024 => 84 }.freeze
end

141
app/models/form/lettings/questions/referral_prp.rb

@ -7,55 +7,104 @@ class Form::Lettings::Questions::ReferralPrp < ::Form::Question
@type = "radio"
@check_answers_card_number = 0
@hint_text = "You told us that the needs type is general needs. We have removed some options because of this."
@answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
ANSWER_OPTIONS = {
"1" => {
"value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
},
"2" => {
"value" => "Tenant applied directly (no nomination)",
},
"3" => {
"value" => "Nominated by a local housing authority",
},
"4" => {
"value" => "Referred by local authority housing department",
},
"8" => {
"value" => "Re-located through official housing mobility scheme",
},
"10" => {
"value" => "Other social landlord",
},
"9" => {
"value" => "Community learning disability team",
},
"14" => {
"value" => "Community mental health team",
},
"15" => {
"value" => "Health service",
},
"12" => {
"value" => "Police, probation or prison",
},
"7" => {
"value" => "Voluntary agency",
},
"13" => {
"value" => "Youth offending team",
},
"17" => {
"value" => "Children’s Social Care",
},
"16" => {
"value" => "Other",
},
}.freeze
def answer_options
if form.start_year_after_2024?
{
"1" => {
"value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
},
"2" => {
"value" => "Tenant applied directly (no nomination)",
},
"3" => {
"value" => "Nominated by a local housing authority",
},
"4" => {
"value" => "Referred by local authority housing department",
},
"8" => {
"value" => "Re-located through official housing mobility scheme",
},
"10" => {
"value" => "Other social landlord",
},
"9" => {
"value" => "Community learning disability team",
},
"14" => {
"value" => "Community mental health team",
},
"15" => {
"value" => "Health service",
},
"18" => {
"value" => "Police, probation, prison or youth offending team – tenant had custodial sentence",
},
"19" => {
"value" => "Police, probation, prison or youth offending team – no custodial sentence",
},
"7" => {
"value" => "Voluntary agency",
},
"17" => {
"value" => "Children’s Social Care",
},
"16" => {
"value" => "Other",
},
}.freeze
else
{
"1" => {
"value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
},
"2" => {
"value" => "Tenant applied directly (no nomination)",
},
"3" => {
"value" => "Nominated by a local housing authority",
},
"4" => {
"value" => "Referred by local authority housing department",
},
"8" => {
"value" => "Re-located through official housing mobility scheme",
},
"10" => {
"value" => "Other social landlord",
},
"9" => {
"value" => "Community learning disability team",
},
"14" => {
"value" => "Community mental health team",
},
"15" => {
"value" => "Health service",
},
"12" => {
"value" => "Police, probation or prison",
},
"7" => {
"value" => "Voluntary agency",
},
"13" => {
"value" => "Youth offending team",
},
"17" => {
"value" => "Children’s Social Care",
},
"16" => {
"value" => "Other",
},
}.freeze
end
end
QUESTION_NUMBER_FROM_YEAR = { 2023 => 85, 2024 => 84 }.freeze
end

132
app/models/form/lettings/questions/referral_supported_housing.rb

@ -7,52 +7,98 @@ class Form::Lettings::Questions::ReferralSupportedHousing < ::Form::Question
@type = "radio"
@check_answers_card_number = 0
@hint_text = "You told us that you are a local authority. We have removed some options because of this."
@answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
ANSWER_OPTIONS = {
"1" => {
"value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
},
"2" => {
"value" => "Tenant applied directly (no referral)",
},
"3" => {
"value" => "Referred by local authority housing department",
},
"8" => {
"value" => "Re-located through official housing mobility scheme",
},
"10" => {
"value" => "Other social landlord",
},
"9" => {
"value" => "Community learning disability team",
},
"14" => {
"value" => "Community mental health team",
},
"15" => {
"value" => "Health service",
},
"12" => {
"value" => "Police, probation or prison",
},
"7" => {
"value" => "Voluntary agency",
},
"13" => {
"value" => "Youth offending team",
},
"17" => {
"value" => "Children’s Social Care",
},
"16" => {
"value" => "Other",
},
}.freeze
def answer_options
if form.start_year_after_2024?
{
"1" => {
"value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
},
"2" => {
"value" => "Tenant applied directly (no referral)",
},
"3" => {
"value" => "Nominated by a local housing authority",
},
"8" => {
"value" => "Re-located through official housing mobility scheme",
},
"10" => {
"value" => "Other social landlord",
},
"9" => {
"value" => "Community learning disability team",
},
"14" => {
"value" => "Community mental health team",
},
"15" => {
"value" => "Health service",
},
"18" => {
"value" => "Police, probation, prison or youth offending team – tenant had custodial sentence",
},
"19" => {
"value" => "Police, probation, prison or youth offending team – no custodial sentence",
},
"7" => {
"value" => "Voluntary agency",
},
"17" => {
"value" => "Children’s Social Care",
},
"16" => {
"value" => "Other",
},
}.freeze
else
{
"1" => {
"value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
},
"2" => {
"value" => "Tenant applied directly (no referral)",
},
"3" => {
"value" => "Nominated by a local housing authority",
},
"8" => {
"value" => "Re-located through official housing mobility scheme",
},
"10" => {
"value" => "Other social landlord",
},
"9" => {
"value" => "Community learning disability team",
},
"14" => {
"value" => "Community mental health team",
},
"15" => {
"value" => "Health service",
},
"12" => {
"value" => "Police, probation or prison",
},
"7" => {
"value" => "Voluntary agency",
},
"13" => {
"value" => "Youth offending team",
},
"17" => {
"value" => "Children’s Social Care",
},
"16" => {
"value" => "Other",
},
}.freeze
end
end
QUESTION_NUMBER_FROM_YEAR = { 2023 => 85, 2024 => 84 }.freeze
end

112
app/models/form/lettings/questions/referral_supported_housing_prp.rb

@ -7,26 +7,104 @@ class Form::Lettings::Questions::ReferralSupportedHousingPrp < ::Form::Question
@type = "radio"
@check_answers_card_number = 0
@hint_text = ""
@answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
ANSWER_OPTIONS = {
"1" => { "value" => "Internal transfer", "hint" => "Where the tenant has moved to another social property owned by the same landlord." },
"2" => { "value" => "Tenant applied directly (no referral or nomination)" },
"3" => { "value" => "Nominated by a local housing authority" },
"4" => { "value" => "Referred by local authority housing department" },
"8" => { "value" => "Re-located through official housing mobility scheme" },
"10" => { "value" => "Other social landlord" },
"9" => { "value" => "Community learning disability team" },
"14" => { "value" => "Community mental health team" },
"15" => { "value" => "Health service" },
"12" => { "value" => "Police, probation or prison" },
"7" => { "value" => "Voluntary agency" },
"13" => { "value" => "Youth offending team" },
"17" => { "value" => "Children’s Social Care" },
"16" => { "value" => "Other" },
}.freeze
def answer_options
if form.start_year_after_2024?
{
"1" => {
"value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
},
"2" => {
"value" => "Tenant applied directly (no referral or nomination)",
},
"3" => {
"value" => "Nominated by a local housing authority",
},
"4" => {
"value" => "Referred by local authority housing department",
},
"8" => {
"value" => "Re-located through official housing mobility scheme",
},
"10" => {
"value" => "Other social landlord",
},
"9" => {
"value" => "Community learning disability team",
},
"14" => {
"value" => "Community mental health team",
},
"15" => {
"value" => "Health service",
},
"18" => {
"value" => "Police, probation, prison or youth offending team – tenant had custodial sentence",
},
"19" => {
"value" => "Police, probation, prison or youth offending team – no custodial sentence",
},
"7" => {
"value" => "Voluntary agency",
},
"17" => {
"value" => "Children’s Social Care",
},
"16" => {
"value" => "Other",
},
}.freeze
else
{
"1" => {
"value" => "Internal transfer",
"hint" => "Where the tenant has moved to another social property owned by the same landlord.",
},
"2" => {
"value" => "Tenant applied directly (no referral or nomination)",
},
"3" => {
"value" => "Nominated by a local housing authority",
},
"4" => {
"value" => "Referred by local authority housing department",
},
"8" => {
"value" => "Re-located through official housing mobility scheme",
},
"10" => {
"value" => "Other social landlord",
},
"9" => {
"value" => "Community learning disability team",
},
"14" => {
"value" => "Community mental health team",
},
"15" => {
"value" => "Health service",
},
"12" => {
"value" => "Police, probation or prison",
},
"7" => {
"value" => "Voluntary agency",
},
"13" => {
"value" => "Youth offending team",
},
"17" => {
"value" => "Children’s Social Care",
},
"16" => {
"value" => "Other",
},
}.freeze
end
end
QUESTION_NUMBER_FROM_YEAR = { 2023 => 85, 2024 => 84 }.freeze
end

4
app/models/form/sales/questions/created_by_id.rb

@ -23,11 +23,11 @@ class Form::Sales::Questions::CreatedById < ::Form::Question
[
(
if log.managing_organisation
log.managing_organisation.absorbing_organisation.present? ? log.managing_organisation&.absorbing_organisation&.users : log.managing_organisation.users
log.managing_organisation.absorbing_organisation.present? ? log.managing_organisation&.absorbing_organisation&.users&.visible : log.managing_organisation.users.visible
end),
].flatten
else
log.managing_organisation.users
log.managing_organisation.users.visible
end.uniq.compact
users.each_with_object(ANSWER_OPTS.dup) do |user, hsh|
hsh[user.id] = present_user(user)

6
app/models/lettings_log.rb

@ -59,11 +59,17 @@ class LettingsLog < Log
query.all
}
scope :search_by, lambda { |param|
by_id = Arel.sql("CASE WHEN lettings_logs.id = #{param.to_i} THEN 0 ELSE 1 END")
by_tenant_code = Arel.sql("CASE WHEN tenancycode = '#{param}' THEN 0 WHEN tenancycode ILIKE '%#{param}%' THEN 1 ELSE 2 END")
by_propcode = Arel.sql("CASE WHEN propcode = '#{param}' THEN 0 WHEN propcode ILIKE '%#{param}%' THEN 1 ELSE 2 END")
by_postcode = Arel.sql("CASE WHEN REPLACE(postcode_full, ' ', '') = '#{param.delete(' ')}' THEN 0 when REPLACE(postcode_full, ' ', '') ILIKE '%#{param.delete(' ')}%' then 1 ELSE 2 END")
filter_by_location_postcode(param)
.or(filter_by_tenant_code(param))
.or(filter_by_propcode(param))
.or(filter_by_postcode(param))
.or(filter_by_id(param.gsub(/log/i, "")))
.order(by_id, by_tenant_code, by_propcode, by_postcode)
}
scope :after_date, ->(date) { where("lettings_logs.startdate >= ?", date) }
scope :before_date, ->(date) { where("lettings_logs.startdate < ?", date) }

5
app/models/sales_log.rb

@ -46,9 +46,14 @@ class SalesLog < Log
}
scope :filter_by_purchaser_code, ->(purchid) { where("purchid ILIKE ?", "%#{purchid}%") }
scope :search_by, lambda { |param|
by_id = Arel.sql("CASE WHEN id = #{param.to_i} THEN 0 ELSE 1 END")
by_purchaser_code = Arel.sql("CASE WHEN purchid = '#{param}' THEN 0 WHEN purchid ILIKE '%#{param}%' THEN 1 ELSE 2 END")
by_postcode = Arel.sql("CASE WHEN REPLACE(postcode_full, ' ', '') = '#{param.delete(' ')}' THEN 0 WHEN REPLACE(postcode_full, ' ', '') ILIKE '%#{param.delete(' ')}%' THEN 1 ELSE 2 END")
filter_by_purchaser_code(param)
.or(filter_by_postcode(param))
.or(filter_by_id(param.gsub(/log/i, "")))
.order(by_id, by_purchaser_code, by_postcode)
}
scope :age1_answered, -> { where.not(age1: nil).or(where(age1_known: [1, 2])) }
scope :duplicate_logs, lambda { |log|

17
app/models/user.rb

@ -76,6 +76,7 @@ class User < ApplicationRecord
scope :not_signed_in, -> { where(last_sign_in_at: nil, active: true) }
scope :deactivated, -> { where(active: false) }
scope :active_status, -> { where(active: true).where.not(last_sign_in_at: nil) }
scope :visible, -> { where(discarded_at: nil) }
def lettings_logs
if support?
@ -238,13 +239,15 @@ class User < ApplicationRecord
end
def status
if active == false
:deactivated
elsif confirmed? == false
:unconfirmed
else
:active
end
return :deleted if discarded_at.present?
return :deactivated unless active
return :unconfirmed unless confirmed?
:active
end
def discard!
update!(discarded_at: Time.zone.now)
end
protected

25
app/policies/user_policy.rb

@ -33,4 +33,29 @@ class UserPolicy
(@current_user == @user || @current_user.data_coordinator? || @current_user.support?) && @user.active?
end
end
def delete_confirmation?
delete?
end
def delete?
return false unless current_user.support?
return false unless user.status == :deactivated
!has_any_logs_in_editable_collection_period && !has_signed_data_protection_agreement?
end
private
def has_any_logs_in_editable_collection_period
editable_from_date = FormHandler.instance.earliest_open_for_editing_collection_start_date
LettingsLog.where(created_by_id: user.id).after_date(editable_from_date).or(LettingsLog.where(startdate: nil, created_by_id: user.id)).any?
end
def has_signed_data_protection_agreement?
return false unless user.is_dpo? && user.organisation.data_protection_confirmed?
user.organisation.data_protection_confirmation.data_protection_officer == user
end
end

6
app/services/feature_toggle.rb

@ -1,6 +1,6 @@
class FeatureToggle
def self.allow_future_form_use?
Rails.env.development? || Rails.env.review? || Rails.env.staging?
false
end
def self.bulk_upload_duplicate_log_check_enabled?
@ -34,4 +34,8 @@ class FeatureToggle
def self.delete_location_enabled?
!Rails.env.production?
end
def self.delete_user_enabled?
!Rails.env.production?
end
end

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

@ -18,6 +18,9 @@
<% row.with_cell(header: true, text: "Last logged in", html_attributes: {
scope: "col",
}) %>
<% row.with_cell(header: true, text: "Status", html_attributes: {
scope: "col",
}) %>
<% end %>
<% end %>
<% users.each do |user| %>
@ -50,7 +53,8 @@
<% end %>
<% end %>
<% row.with_cell(text: simple_format(org_cell(user), {}, wrapper_tag: "div")) %>
<% row.with_cell(text: user.active? ? user.last_sign_in_at&.to_formatted_s(:govuk_date) : status_tag(user.status)) %>
<% row.with_cell(text: user.last_sign_in_at&.to_formatted_s(:govuk_date)) %>
<% row.with_cell(text: status_tag(user.status)) %>
<%= govuk_link_to users_path(user) do %>
<span class="govuk-visually-hidden">User </span><%= user.id %>
<% end %>

24
app/views/users/delete_confirmation.html.erb

@ -0,0 +1,24 @@
<% content_for :before_content do %>
<% content_for :title, "Are you sure you want to delete this user?" %>
<%= govuk_back_link(href: :back) %>
<% end %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<span class="govuk-caption-xl">Delete <%= @user.name %></span>
<h1 class="govuk-heading-xl">
<%= content_for(:title) %>
</h1>
<%= govuk_warning_text(text: "You will not be able to undo this action.") %>
<div class="govuk-button-group">
<%= govuk_button_to(
"Delete this user",
delete_user_path(@user),
method: :delete,
) %>
<%= govuk_button_link_to "Cancel", user_path(@user), html: { method: :get }, secondary: true %>
</div>
</div>
</div>

18
app/views/users/show.html.erb

@ -112,11 +112,18 @@
row.with_action
end
end %>
<%= summary_list.with_row do |row| %>
<% row.with_key { "Status" } %>
<% row.with_value do %>
<%= details_html({ name: "Status", value: status_tag(@user.status), id: "status" }) %>
<% if @user.status == :deactivated && current_user.support? && !UserPolicy.new(current_user, @user).delete? %>
<span class="app-!-colour-muted">This user was active in an open or editable collection year, and cannot be deleted.</span>
<% end %>
<% end %>
<% end %>
<%= summary_list.with_row do |row|
row.with_key { "Status" }
row.with_value { status_tag(@user.status) }
row.with_action
row.with_key { "Last logged in" }
row.with_value { @user.last_sign_in_at&.to_formatted_s(:govuk_date) }
end %>
<% end %>
@ -133,6 +140,9 @@
</span>
<% end %>
<% end %>
<% if UserPolicy.new(current_user, @user).delete? && FeatureToggle.delete_user_enabled? %>
<%= delete_user_link(@user) %>
<% end %>
</div>
</div>
</div>

1
config/locales/en.yml

@ -198,6 +198,7 @@ en:
other: "There are %{count} sets of duplicate logs"
location_deleted: "%{postcode} has been deleted."
scheme_deleted: "%{service_name} has been deleted."
user_deleted: "%{name} has been deleted."
validations:
organisation:

2
config/routes.rb

@ -129,6 +129,8 @@ Rails.application.routes.draw do
get "deactivate", to: "users#deactivate"
get "reactivate", to: "users#reactivate"
post "resend-invite", to: "users#resend_invite"
get "delete-confirmation", to: "users#delete_confirmation"
delete "delete", to: "users#delete"
end
end

5
db/migrate/20240305091927_add_discarded_at_column_to_users.rb

@ -0,0 +1,5 @@
class AddDiscardedAtColumnToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :discarded_at, :datetime
end
end

1
db/schema.rb

@ -763,6 +763,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_03_19_122706) do
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.boolean "initial_confirmation_sent"
t.datetime "discarded_at"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["encrypted_otp_secret_key"], name: "index_users_on_encrypted_otp_secret_key", unique: true

8
spec/features/user_spec.rb

@ -480,14 +480,14 @@ RSpec.describe "User Features" do
it "allows to cancel user deactivation" do
click_link("No – I’ve changed my mind")
expect(page).to have_current_path("/users/#{other_user.id}")
expect(page).to have_no_content("This user has been deactivated.")
assert_selector ".govuk-tag", text: /Deactivated/, count: 0
expect(page).to have_no_css(".govuk-notification-banner.govuk-notification-banner--success")
end
it "allows to deactivate the user" do
click_button("I’m sure – deactivate this user")
expect(page).to have_current_path("/users/#{other_user.id}")
expect(page).to have_content("Deactivated")
assert_selector ".govuk-tag", text: /Deactivated/, count: 1
expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success")
end
end
@ -517,7 +517,7 @@ RSpec.describe "User Features" do
it "allows to cancel user reactivation" do
click_link("No – I’ve changed my mind")
expect(page).to have_current_path("/users/#{other_user.id}")
expect(page).to have_content("Deactivated")
assert_selector ".govuk-tag", text: /Deactivated/, count: 1
expect(page).to have_no_css(".govuk-notification-banner.govuk-notification-banner--success")
end
@ -525,7 +525,7 @@ RSpec.describe "User Features" do
expect(notify_client).to receive(:send_email).with(email_address: other_user.email, template_id: User::USER_REACTIVATED_TEMPLATE_ID, personalisation:).once
click_button("I’m sure – reactivate this user")
expect(page).to have_current_path("/users/#{other_user.id}")
expect(page).to have_no_content("This user has been deactivated.")
assert_selector ".govuk-tag", text: /Deactivated/, count: 0
expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success")
end
end

21
spec/models/form/lettings/questions/created_by_id_spec.rb

@ -60,6 +60,17 @@ RSpec.describe Form::Lettings::Questions::CreatedById, type: :model do
it "only displays users that belong to owning and managing organisations" do
expect(question.displayed_answer_options(lettings_log, support_user)).to eq(expected_option_for_users(managing_org_user.organisation.users + owning_org_user.organisation.users))
end
context "when organisation has deleted users" do
before do
create(:user, name: "Deleted user", discarded_at: Time.zone.yesterday, organisation: owning_org_user.organisation)
create(:user, name: "Deleted managing user", discarded_at: Time.zone.yesterday, organisation: managing_org_user.organisation)
end
it "does not display deleted users" do
expect(question.displayed_answer_options(lettings_log, support_user)).to eq(expected_option_for_users(managing_org_user.organisation.users.visible + owning_org_user.organisation.users.visible))
end
end
end
end
@ -77,6 +88,16 @@ RSpec.describe Form::Lettings::Questions::CreatedById, type: :model do
it "only displays users that belong user's org" do
expect(question.displayed_answer_options(lettings_log, data_coordinator)).to eq(expected_option_for_users(data_coordinator.organisation.users))
end
context "when organisation has deleted users" do
before do
create(:user, name: "Deleted user", discarded_at: Time.zone.yesterday, organisation: data_coordinator.organisation)
end
it "does not display deleted users" do
expect(question.displayed_answer_options(lettings_log, data_coordinator)).to eq(expected_option_for_users(data_coordinator.organisation.users.visible))
end
end
end
end
end

101
spec/models/form/lettings/questions/referral_spec.rb

@ -0,0 +1,101 @@
require "rails_helper"
RSpec.describe Form::Lettings::Questions::Referral, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) }
let(:subsection) { instance_double(Form::Subsection) }
let(:form) { instance_double(Form, start_date: Time.zone.local(2023, 4, 1)) }
before do
allow(form).to receive(:start_year_after_2024?).and_return(false)
allow(page).to receive(:subsection).and_return(subsection)
allow(subsection).to receive(:form).and_return(form)
end
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("referral")
end
it "has the correct header" do
expect(question.header).to eq("What was the source of referral for this letting?")
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Source of referral for letting")
end
it "has the correct type" do
expect(question.type).to eq("radio")
end
it "has the correct check_answers_card_number" do
expect(question.check_answers_card_number).to eq(0)
end
it "has the correct hint" do
expect(question.hint_text).to eq("You told us that you are a local authority and that the needs type is general needs. We have removed some options because of this.")
end
it "is not marked as derived" do
expect(question).not_to be_derived(nil)
end
context "with 2023/24 form" do
it "has the correct answer_options" do
expect(question.answer_options).to eq({
"1" => { "value" => "Internal transfer", "hint" => "Where the tenant has moved to another social property owned by the same landlord." },
"2" => { "value" => "Tenant applied directly (no referral or nomination)" },
"8" => { "value" => "Re-located through official housing mobility scheme" },
"10" => { "value" => "Other social landlord" },
"9" => { "value" => "Community learning disability team" },
"14" => { "value" => "Community mental health team" },
"15" => { "value" => "Health service" },
"12" => { "value" => "Police, probation or prison" },
"7" => { "value" => "Voluntary agency" },
"13" => { "value" => "Youth offending team" },
"17" => { "value" => "Children’s Social Care" },
"16" => { "value" => "Other" },
})
end
it "has the correct question number" do
expect(question.question_number).to eq(85)
end
end
context "with 2024/25 form" do
let(:form) { instance_double(Form, start_date: Time.zone.local(2024, 4, 1)) }
before do
allow(form).to receive(:start_year_after_2024?).and_return(true)
end
it "has the correct answer_options" do
expect(question.answer_options).to eq({
"1" => { "value" => "Internal transfer", "hint" => "Where the tenant has moved to another social property owned by the same landlord." },
"2" => { "value" => "Tenant applied directly (no referral or nomination)" },
"8" => { "value" => "Re-located through official housing mobility scheme" },
"10" => { "value" => "Other social landlord" },
"9" => { "value" => "Community learning disability team" },
"14" => { "value" => "Community mental health team" },
"15" => { "value" => "Health service" },
"18" => { "value" => "Police, probation, prison or youth offending team – tenant had custodial sentence" },
"19" => { "value" => "Police, probation, prison or youth offending team – no custodial sentence" },
"7" => { "value" => "Voluntary agency" },
"17" => { "value" => "Children’s Social Care" },
"16" => { "value" => "Other" },
})
end
it "has the correct question number" do
expect(question.question_number).to eq(84)
end
end
end

20
spec/models/form/sales/questions/created_by_id_spec.rb

@ -57,6 +57,16 @@ RSpec.describe Form::Sales::Questions::CreatedById, type: :model do
it "only displays users that belong to the managing organisation" do
expect(question.displayed_answer_options(sales_log, support_user)).to eq(expected_option_for_users(owning_org_user.organisation.users))
end
context "when organisation has deleted users" do
before do
create(:user, name: "Deleted user", discarded_at: Time.zone.yesterday, organisation: owning_org_user.organisation)
end
it "does not display deleted users" do
expect(question.displayed_answer_options(sales_log, support_user)).to eq(expected_option_for_users(owning_org_user.organisation.users.visible))
end
end
end
end
@ -78,6 +88,16 @@ RSpec.describe Form::Sales::Questions::CreatedById, type: :model do
it "only displays users that belong to managing organisation" do
expect(question.displayed_answer_options(sales_log, data_coordinator)).to eq(expected_option_for_users(owning_org_user.organisation.users))
end
context "when organisation has deleted users" do
before do
create(:user, name: "Deleted user", discarded_at: Time.zone.yesterday, organisation: owning_org_user.organisation)
end
it "does not display deleted users" do
expect(question.displayed_answer_options(sales_log, data_coordinator)).to eq(expected_option_for_users(owning_org_user.organisation.users.visible))
end
end
end
end
end

24
spec/models/lettings_log_spec.rb

@ -2885,6 +2885,30 @@ RSpec.describe LettingsLog do
expect(result.first.id).to eq lettings_log_to_search.id
end
end
context "when matching multiple records on different fields" do
let!(:lettings_log_with_propcode) { create(:lettings_log, propcode: lettings_log_to_search.id) }
let!(:lettings_log_with_tenancycode) { create(:lettings_log, tenancycode: lettings_log_to_search.id) }
let!(:lettings_log_with_postcode) { create(:lettings_log, postcode_full: "C1 1AC") }
let!(:lettings_log_with_postcode_tenancycode) { create(:lettings_log, tenancycode: "C1 1AC") }
let!(:lettings_log_with_postcode_propcode) { create(:lettings_log, propcode: "C1 1AC") }
it "returns all matching records in correct order with matching IDs" do
result = described_class.search_by(lettings_log_to_search.id.to_s)
expect(result.count).to eq(3)
expect(result.first.id).to eq lettings_log_to_search.id
expect(result.second.id).to eq lettings_log_with_tenancycode.id
expect(result.third.id).to eq lettings_log_with_propcode.id
end
it "returns all matching records in correct order with matching postcode" do
result = described_class.search_by("C1 1AC")
expect(result.count).to eq(3)
expect(result.first.id).to eq lettings_log_with_postcode_tenancycode.id
expect(result.second.id).to eq lettings_log_with_postcode_propcode.id
expect(result.third.id).to eq lettings_log_with_postcode.id
end
end
end
end

94
spec/models/sales_log_spec.rb

@ -480,7 +480,7 @@ RSpec.describe SalesLog, type: :model do
context "when there is a log with nil values for purchid" do
let!(:purchid_not_given) { create(:sales_log, :duplicate, purchid: nil, owning_organisation: organisation) }
it "returns the log as a duplicate if tenancy code is nil" do
it "returns the log as a duplicate if purchaser code is nil" do
log.update!(purchid: nil)
expect(duplicate_sets.count).to eq(1)
expect(duplicate_sets.first).to contain_exactly(log.id, purchid_not_given.id)
@ -1142,5 +1142,97 @@ RSpec.describe SalesLog, type: :model do
end
end
end
context "when searching logs" do
let!(:sales_log_to_search) { create(:sales_log, :completed, purchid: "to search", postcode_full: "ME0 0WW") }
before do
create_list(:sales_log, 5, :completed)
end
describe "#filter_by_id" do
it "allows searching by a log ID" do
result = described_class.filter_by_id(sales_log_to_search.id.to_s)
expect(result.count).to eq(1)
expect(result.first.id).to eq sales_log_to_search.id
end
end
describe "#filter_by_purchaser_code" do
it "allows searching by a purchaser_code" do
result = described_class.filter_by_purchaser_code(sales_log_to_search.purchid)
expect(result.count).to eq(1)
expect(result.first.id).to eq sales_log_to_search.id
end
context "when purchaser_code has lower case letters" do
let(:matching_purchaser_code_lower_case) { sales_log_to_search.purchid.downcase }
it "allows searching by a purchaser_code" do
result = described_class.filter_by_purchaser_code(matching_purchaser_code_lower_case)
expect(result.count).to eq(1)
expect(result.first.id).to eq sales_log_to_search.id
end
end
end
describe "#filter_by_postcode" do
it "allows searching by a Property Postcode" do
result = described_class.filter_by_postcode(sales_log_to_search.postcode_full)
expect(result.count).to eq(1)
expect(result.first.id).to eq sales_log_to_search.id
end
end
describe "#search_by" do
it "allows searching using ID" do
result = described_class.search_by(sales_log_to_search.id.to_s)
expect(result.count).to eq(1)
expect(result.first.id).to eq sales_log_to_search.id
end
it "allows searching using purchaser code" do
result = described_class.search_by(sales_log_to_search.purchid)
expect(result.count).to eq(1)
expect(result.first.id).to eq sales_log_to_search.id
end
it "allows searching by a Property Postcode" do
result = described_class.search_by(sales_log_to_search.postcode_full)
expect(result.count).to eq(1)
expect(result.first.id).to eq sales_log_to_search.id
end
context "when postcode has spaces and lower case letters" do
let(:matching_postcode_lower_case_with_spaces) { sales_log_to_search.postcode_full.downcase.chars.insert(3, " ").join }
it "allows searching by a Property Postcode" do
result = described_class.search_by(matching_postcode_lower_case_with_spaces)
expect(result.count).to eq(1)
expect(result.first.id).to eq sales_log_to_search.id
end
end
context "when matching multiple records on different fields" do
let!(:sales_log_with_purchid) { create(:sales_log, purchid: sales_log_to_search.id) }
let!(:sales_log_with_postcode) { create(:sales_log, postcode_full: "C1 1AC") }
let!(:sales_log_with_postcode_purchid) { create(:sales_log, purchid: "C1 1AC") }
it "returns all matching records in correct order with matching IDs" do
result = described_class.search_by(sales_log_to_search.id.to_s)
expect(result.count).to eq(2)
expect(result.first.id).to eq sales_log_to_search.id
expect(result.second.id).to eq sales_log_with_purchid.id
end
it "returns all matching records in correct order with matching postcode" do
result = described_class.search_by("C1 1AC")
expect(result.count).to eq(2)
expect(result.first.id).to eq sales_log_with_postcode_purchid.id
expect(result.second.id).to eq sales_log_with_postcode.id
end
end
end
end
end
# rubocop:enable RSpec/MessageChain

9
spec/models/user_spec.rb

@ -517,5 +517,14 @@ RSpec.describe User, type: :model do
expect(user.status).to eq(:active)
end
context "when the user is deleted" do
let(:user) { create(:user, discarded_at: Time.zone.yesterday) }
it "returns the status of the user" do
user.destroy!
expect(user.status).to eq(:deleted)
end
end
end
end

124
spec/policies/user_policy_spec.rb

@ -99,5 +99,129 @@ RSpec.describe UserPolicy do
expect(policy).to permit(support, data_provider)
end
end
permissions :delete? do
context "with active user" do
let(:user) { create(:user, last_sign_in_at: Time.zone.yesterday) }
it "does not allow deleting a user as a provider" do
expect(user.status).to eq(:active)
expect(policy).not_to permit(data_provider, user)
end
it "does not allow allows deleting a user as a coordinator" do
expect(policy).not_to permit(data_coordinator, user)
end
it "does not allow deleting a user as a support user" do
expect(policy).not_to permit(support, user)
end
end
context "with unconfirmed user" do
let(:user) { create(:user) }
before do
user.confirmed_at = nil
user.save!(validate: false)
end
it "does not allow deleting a user as a provider" do
expect(user.status).to eq(:unconfirmed)
expect(policy).not_to permit(data_provider, user)
end
it "does not allow allows deleting a user as a coordinator" do
expect(policy).not_to permit(data_coordinator, user)
end
it "does not allow deleting a user as a support user" do
expect(policy).not_to permit(support, user)
end
end
context "with deactivated user" do
let(:user) { create(:user, active: false) }
before do
Timecop.freeze(Time.utc(2024, 4, 10))
log = create(:lettings_log, owning_organisation: user.organisation, created_by: user)
log.startdate = Time.zone.local(2022, 10, 10)
log.save!(validate: false)
end
after do
Timecop.unfreeze
end
context "and associated logs in editable collection period" do
before do
create(:lettings_log, :sh, owning_organisation: user.organisation, created_by: user, startdate: Time.zone.local(2024, 4, 9))
end
it "does not allow deleting a user as a provider" do
expect(policy).not_to permit(data_provider, user)
end
it "does not allow allows deleting a user as a coordinator" do
expect(policy).not_to permit(data_coordinator, user)
end
it "does not allow deleting a user as a support user" do
expect(policy).not_to permit(support, user)
end
end
context "and no associated logs in editable collection period" do
it "does not allow deleting a user as a provider" do
expect(policy).not_to permit(data_provider, user)
end
it "does not allow allows deleting a user as a coordinator" do
expect(policy).not_to permit(data_coordinator, user)
end
it "allows deleting a user as a support user" do
expect(policy).to permit(support, user)
end
end
context "and user is the DPO that has signed the agreement" do
let(:user) { create(:user, active: false, is_dpo: true) }
before do
user.organisation.data_protection_confirmation.update!(data_protection_officer: user)
end
it "does not allow deleting a user as a provider" do
expect(policy).not_to permit(data_provider, user)
end
it "does not allow allows deleting a user as a coordinator" do
expect(policy).not_to permit(data_coordinator, user)
end
it "does not allow deleting a user as a support user" do
expect(policy).not_to permit(support, user)
end
end
context "and user is the DPO that hasn't signed the agreement" do
let(:user) { create(:user, active: false, is_dpo: true) }
it "does not allow deleting a user as a provider" do
expect(policy).not_to permit(data_provider, user)
end
it "does not allow allows deleting a user as a coordinator" do
expect(policy).not_to permit(data_coordinator, user)
end
it "allows deleting a user as a support user" do
expect(policy).to permit(support, user)
end
end
end
end
end
# rubocop:enable RSpec/RepeatedExample

260
spec/requests/users_controller_spec.rb

@ -103,13 +103,30 @@ RSpec.describe UsersController, type: :request do
expect(response).to redirect_to(new_user_session_path)
end
end
describe "#delete-confirmation" do
it "redirects to the sign in page" do
get "/users/#{user.id}/delete-confirmation"
expect(response).to redirect_to("/account/sign-in")
end
end
describe "#delete" do
it "redirects to the sign in page" do
delete "/users/#{user.id}/delete"
expect(response).to redirect_to("/account/sign-in")
end
end
end
context "when user is signed in as a data provider" do
before do
sign_in user
end
describe "#show" do
context "when the current user matches the user ID" do
before do
sign_in user
get "/users/#{user.id}", headers:, params: {}
end
@ -155,7 +172,6 @@ RSpec.describe UsersController, type: :request do
let(:user) { create(:user, role: nil) }
before do
sign_in user
get "/users/#{user.id}", headers:, params: {}
end
@ -166,7 +182,6 @@ RSpec.describe UsersController, type: :request do
context "when the current user does not match the user ID" do
before do
sign_in user
get "/users/#{other_user.id}", headers:, params: {}
end
@ -223,7 +238,6 @@ RSpec.describe UsersController, type: :request do
describe "#edit" do
context "when the current user matches the user ID" do
before do
sign_in user
get "/users/#{user.id}/edit", headers:, params: {}
end
@ -242,7 +256,6 @@ RSpec.describe UsersController, type: :request do
context "when the current user does not match the user ID" do
before do
sign_in user
get "/users/#{other_user.id}/edit", headers:, params: {}
end
@ -255,7 +268,6 @@ RSpec.describe UsersController, type: :request do
describe "#edit_password" do
context "when the current user matches the user ID" do
before do
sign_in user
get "/account/edit/password", headers:, params: {}
end
@ -270,7 +282,6 @@ RSpec.describe UsersController, type: :request do
context "when the current user does not match the user ID" do
before do
sign_in user
get "/users/#{other_user.id}/edit", headers:, params: {}
end
@ -283,7 +294,6 @@ RSpec.describe UsersController, type: :request do
describe "#update" do
context "when the current user matches the user ID" do
before do
sign_in user
patch "/users/#{user.id}", headers:, params:
end
@ -313,7 +323,6 @@ RSpec.describe UsersController, type: :request do
context "when the update fails to persist" do
before do
sign_in user
allow(User).to receive(:find_by).and_return(user)
allow(user).to receive(:update).and_return(false)
patch "/users/#{user.id}", headers:, params:
@ -328,7 +337,6 @@ RSpec.describe UsersController, type: :request do
let(:params) { { id: other_user.id, user: { name: new_name } } }
before do
sign_in user
patch "/users/#{other_user.id}", headers:, params:
end
@ -345,7 +353,6 @@ RSpec.describe UsersController, type: :request do
end
before do
sign_in user
patch "/users/#{user.id}", headers:, params:
end
@ -368,10 +375,6 @@ RSpec.describe UsersController, type: :request do
end
let(:request) { post "/users/", headers:, params: }
before do
sign_in user
end
it "does not invite a new user" do
expect { request }.not_to change(User, :count)
end
@ -381,17 +384,37 @@ RSpec.describe UsersController, type: :request do
expect(response).to have_http_status(:unauthorized)
end
end
describe "#delete-confirmation" do
before do
get "/users/#{user.id}/delete-confirmation"
end
it "returns 401 unauthorized" do
expect(response).to have_http_status(:unauthorized)
end
end
describe "#delete" do
before do
delete "/users/#{user.id}/delete"
end
it "returns 401 unauthorized" do
expect(response).to have_http_status(:unauthorized)
end
end
end
context "when user is signed in as a data coordinator" do
let(:user) { create(:user, :data_coordinator, email: "coordinator@example.com", organisation: create(:organisation, :without_dpc)) }
let!(:other_user) { create(:user, organisation: user.organisation, name: "filter name", email: "filter@example.com") }
describe "#index" do
before do
sign_in user
end
before do
sign_in user
end
describe "#index" do
context "when there are no url params" do
before do
get "/users", headers:, params: {}
@ -532,7 +555,6 @@ RSpec.describe UsersController, type: :request do
let(:user) { create(:user) }
before do
sign_in user
get "/users", headers:, params: {}
end
@ -544,7 +566,6 @@ RSpec.describe UsersController, type: :request do
describe "#show" do
context "when the current user matches the user ID" do
before do
sign_in user
get "/users/#{user.id}", headers:, params: {}
end
@ -579,12 +600,15 @@ RSpec.describe UsersController, type: :request do
it "does not allow resending invitation emails" do
expect(page).not_to have_button("Resend invite link")
end
it "does not allow deleting the the user" do
expect(page).not_to have_link("Delete this user", href: "/users/#{user.id}/delete-confirmation")
end
end
end
context "when the current user does not match the user ID" do
before do
sign_in user
get "/users/#{other_user.id}", headers:, params: {}
end
@ -622,7 +646,7 @@ RSpec.describe UsersController, type: :request do
end
it "shows if user is not active" do
expect(page).to have_content("Deactivated")
assert_select ".govuk-tag", text: /Deactivated/, count: 1
end
it "allows reactivating the user" do
@ -652,7 +676,6 @@ RSpec.describe UsersController, type: :request do
describe "#edit" do
context "when the current user matches the user ID" do
before do
sign_in user
get "/users/#{user.id}/edit", headers:, params: {}
end
@ -673,7 +696,6 @@ RSpec.describe UsersController, type: :request do
context "when the current user does not match the user ID" do
before do
sign_in user
get "/users/#{other_user.id}/edit", headers:, params: {}
end
@ -706,7 +728,6 @@ RSpec.describe UsersController, type: :request do
describe "#edit_password" do
context "when the current user matches the user ID" do
before do
sign_in user
get "/account/edit/password", headers:, params: {}
end
@ -720,10 +741,6 @@ RSpec.describe UsersController, type: :request do
end
context "when the current user does not match the user ID" do
before do
sign_in user
end
it "there is no route" do
expect {
get "/users/#{other_user.id}/password/edit", headers:, params: {}
@ -735,7 +752,6 @@ RSpec.describe UsersController, type: :request do
describe "#update" do
context "when the current user matches the user ID" do
before do
sign_in user
patch "/users/#{user.id}", headers:, params:
end
@ -770,7 +786,6 @@ RSpec.describe UsersController, type: :request do
end
before do
sign_in user
patch "/users/#{user.id}", headers:, params:
end
@ -782,10 +797,6 @@ RSpec.describe UsersController, type: :request do
end
context "when the current user does not match the user ID" do
before do
sign_in user
end
context "when the user is part of the same organisation as the current user" do
it "updates the user" do
expect { patch "/users/#{other_user.id}", headers:, params: }
@ -871,7 +882,6 @@ RSpec.describe UsersController, type: :request do
let(:params) { { id: other_user.id, user: { name: new_name } } }
before do
sign_in user
patch "/users/#{other_user.id}", headers:, params:
end
@ -884,7 +894,6 @@ RSpec.describe UsersController, type: :request do
context "when the update fails to persist" do
before do
sign_in user
allow(User).to receive(:find_by).and_return(user)
allow(user).to receive(:update).and_return(false)
patch "/users/#{user.id}", headers:, params:
@ -905,7 +914,6 @@ RSpec.describe UsersController, type: :request do
end
before do
sign_in user
patch "/users/#{user.id}", headers:, params:
end
@ -977,10 +985,6 @@ RSpec.describe UsersController, type: :request do
end
let(:request) { post "/users/", headers:, params: }
before do
sign_in user
end
it "invites a new user" do
expect { request }.to change(User, :count).by(1)
end
@ -1102,10 +1106,6 @@ RSpec.describe UsersController, type: :request do
end
describe "#new" do
before do
sign_in user
end
it "cannot assign support role to the new user" do
get "/users/new"
expect(page).not_to have_field("user-role-support-field")
@ -1113,10 +1113,6 @@ RSpec.describe UsersController, type: :request do
end
describe "#deactivate" do
before do
sign_in user
end
context "when the current user matches the user ID" do
before do
get "/users/#{user.id}/deactivate", headers:, params: {}
@ -1143,10 +1139,6 @@ RSpec.describe UsersController, type: :request do
end
describe "#reactivate" do
before do
sign_in user
end
context "when the current user does not match the user ID" do
before do
other_user.update!(active: false)
@ -1162,6 +1154,26 @@ RSpec.describe UsersController, type: :request do
end
end
end
describe "#delete-confirmation" do
before do
get "/users/#{user.id}/delete-confirmation"
end
it "returns 401 unauthorized" do
expect(response).to have_http_status(:unauthorized)
end
end
describe "#delete" do
before do
delete "/users/#{user.id}/delete"
end
it "returns 401 unauthorized" do
expect(response).to have_http_status(:unauthorized)
end
end
end
context "when user is signed in as a support user" do
@ -1170,15 +1182,15 @@ RSpec.describe UsersController, type: :request do
before do
allow(user).to receive(:need_two_factor_authentication?).and_return(false)
sign_in user
end
describe "#index" do
let!(:other_user) { create(:user, organisation: user.organisation, name: "User 2", email: "other@example.com") }
let!(:inactive_user) { create(:user, organisation: user.organisation, active: false, name: "User 3", email: "inactive@example.com") }
let!(:inactive_user) { create(:user, organisation: user.organisation, active: false, name: "User 3", email: "inactive@example.com", last_sign_in_at: Time.zone.local(2022, 10, 10)) }
let!(:other_org_user) { create(:user, name: "User 4", email: "otherorg@otherexample.com", organisation: create(:organisation, :without_dpc)) }
before do
sign_in user
get "/users", headers:, params: {}
end
@ -1189,7 +1201,11 @@ RSpec.describe UsersController, type: :request do
expect(page).to have_content(other_org_user.name)
end
it "shows last logged in as deactivated for inactive users" do
it "shows last logged in date for all users" do
expect(page).to have_content("10 October 2022")
end
it "shows status tag as deactivated for inactive users" do
expect(page).to have_content("Deactivated")
end
@ -1326,7 +1342,6 @@ RSpec.describe UsersController, type: :request do
before do
create_list(:user, 25)
sign_in user
end
context "when there is no search param" do
@ -1371,7 +1386,6 @@ RSpec.describe UsersController, type: :request do
describe "#show" do
context "when the current user matches the user ID" do
before do
sign_in user
get "/users/#{user.id}", headers:, params: {}
end
@ -1396,7 +1410,6 @@ RSpec.describe UsersController, type: :request do
context "when the current user does not match the user ID" do
before do
sign_in user
get "/users/#{other_user.id}", headers:, params: {}
end
@ -1427,6 +1440,10 @@ RSpec.describe UsersController, type: :request do
expect(page).to have_link("Deactivate user", href: "/users/#{other_user.id}/deactivate")
end
it "does not alow deleting the the user" do
expect(page).not_to have_link("Delete this user", href: "/users/#{other_user.id}/delete-confirmation")
end
context "when user never logged in" do
before do
other_user.update!(last_sign_in_at: nil)
@ -1458,6 +1475,10 @@ RSpec.describe UsersController, type: :request do
it "allows you to resend invitation emails" do
expect(page).to have_button("Resend invite link")
end
it "does not allow deleting the the user" do
expect(page).not_to have_link("Delete this user", href: "/users/#{other_user.id}/delete-confirmation")
end
end
context "when user is deactivated" do
@ -1467,12 +1488,39 @@ RSpec.describe UsersController, type: :request do
end
it "shows if user is not active" do
expect(page).to have_content("Deactivated")
assert_select ".govuk-tag", text: /Deactivated/, count: 1
end
it "allows reactivating the user" do
expect(page).to have_link("Reactivate user", href: "/users/#{other_user.id}/reactivate")
end
it "allows deleting the the user" do
expect(page).to have_link("Delete this user", href: "/users/#{other_user.id}/delete-confirmation")
end
it "does not render informative text about deleting the user" do
expect(response).to have_http_status(:ok)
expect(page).not_to have_content("This user was active in an open or editable collection year, and cannot be deleted.")
end
context "and has associated logs in editable collection period" do
before do
create(:data_protection_confirmation, organisation: other_user.organisation, confirmed: true)
create(:lettings_log, owning_organisation: other_user.organisation, created_by: other_user)
get "/users/#{other_user.id}"
end
it "does not render delete this user" do
expect(response).to have_http_status(:ok)
expect(page).not_to have_link("Delete this user", href: "/users/#{user.id}/delete-confirmation")
end
it "adds informative text about deleting the user" do
expect(response).to have_http_status(:ok)
expect(page).to have_content("This user was active in an open or editable collection year, and cannot be deleted.")
end
end
end
end
@ -1503,7 +1551,6 @@ RSpec.describe UsersController, type: :request do
describe "#edit" do
context "when the current user matches the user ID" do
before do
sign_in user
get "/users/#{user.id}/edit", headers:, params: {}
end
@ -1525,7 +1572,6 @@ RSpec.describe UsersController, type: :request do
context "when the current user does not match the user ID" do
before do
sign_in user
get "/users/#{other_user.id}/edit", headers:, params: {}
end
@ -1581,7 +1627,6 @@ RSpec.describe UsersController, type: :request do
describe "#edit_password" do
context "when the current user matches the user ID" do
before do
sign_in user
get "/account/edit/password", headers:, params: {}
end
@ -1595,10 +1640,6 @@ RSpec.describe UsersController, type: :request do
end
context "when the current user does not match the user ID" do
before do
sign_in user
end
it "there is no route" do
expect {
get "/users/#{other_user.id}/password/edit", headers:, params: {}
@ -1611,10 +1652,6 @@ RSpec.describe UsersController, type: :request do
context "when the current user matches the user ID" do
let(:request) { patch "/users/#{user.id}", headers:, params: }
before do
sign_in user
end
it "updates the user" do
request
user.reload
@ -1726,7 +1763,6 @@ RSpec.describe UsersController, type: :request do
end
before do
sign_in user
patch "/users/#{user.id}", headers:, params:
end
@ -1738,10 +1774,6 @@ RSpec.describe UsersController, type: :request do
end
context "when the current user does not match the user ID" do
before do
sign_in user
end
context "when the user is part of the same organisation as the current user" do
it "updates the user" do
expect { patch "/users/#{other_user.id}", headers:, params: }
@ -1796,10 +1828,6 @@ RSpec.describe UsersController, type: :request do
let(:other_user) { create(:user) }
let(:params) { { id: other_user.id, user: { name: new_name } } }
before do
sign_in user
end
it "updates the user" do
expect { patch "/users/#{other_user.id}", headers:, params: }
.to change { other_user.reload.name }.from(other_user.name).to(new_name)
@ -1886,7 +1914,6 @@ RSpec.describe UsersController, type: :request do
context "when the update fails to persist" do
before do
sign_in user
allow(User).to receive(:find_by).and_return(user)
allow(user).to receive(:update).and_return(false)
patch "/users/#{user.id}", headers:, params:
@ -1914,10 +1941,6 @@ RSpec.describe UsersController, type: :request do
end
let(:request) { post "/users/", headers:, params: }
before do
sign_in user
end
it "invites a new user" do
expect { request }.to change(User, :count).by(1)
end
@ -1990,7 +2013,6 @@ RSpec.describe UsersController, type: :request do
describe "#new" do
before do
sign_in user
create(:organisation, name: "other org")
end
@ -2018,6 +2040,68 @@ RSpec.describe UsersController, type: :request do
end
end
end
describe "#delete-confirmation" do
let(:other_user) { create(:user, active: false) }
before do
get "/users/#{other_user.id}/delete-confirmation"
end
it "shows the correct title" do
expect(page.find("h1").text).to include "Are you sure you want to delete this user?"
end
it "shows a warning to the user" do
expect(page).to have_selector(".govuk-warning-text", text: "You will not be able to undo this action")
end
it "shows a button to delete the selected user" do
expect(page).to have_selector("form.button_to button", text: "Delete this user")
end
it "the delete user button submits the correct data to the correct path" do
form_containing_button = page.find("form.button_to")
expect(form_containing_button[:action]).to eq delete_user_path(other_user)
expect(form_containing_button).to have_field "_method", type: :hidden, with: "delete"
end
it "shows a cancel link with the correct style" do
expect(page).to have_selector("a.govuk-button--secondary", text: "Cancel")
end
it "shows cancel link that links back to the user page" do
expect(page).to have_link(text: "Cancel", href: user_path(other_user))
end
end
describe "#delete" do
let(:other_user) { create(:user, name: "User to be deleted", active: false) }
before do
delete "/users/#{other_user.id}/delete"
end
it "deletes the user" do
other_user.reload
expect(other_user.status).to eq(:deleted)
expect(other_user.discarded_at).not_to be nil
end
it "redirects to the users list and displays a notice that the user has been deleted" do
expect(response).to redirect_to users_organisation_path(other_user.organisation)
follow_redirect!
expect(page).to have_selector(".govuk-notification-banner--success")
expect(page).to have_selector(".govuk-notification-banner--success", text: "User to be deleted has been deleted.")
end
it "does not display the deleted user" do
expect(response).to redirect_to users_organisation_path(other_user.organisation)
follow_redirect!
expect(page).not_to have_link("User to be deleted")
end
end
end
describe "title link" do

54
yarn.lock

@ -1771,25 +1771,7 @@ bl@^4.1.0:
inherits "^2.0.4"
readable-stream "^3.4.0"
body-parser@1.20.1:
version "1.20.1"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
dependencies:
bytes "3.1.2"
content-type "~1.0.4"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
http-errors "2.0.0"
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.11.0"
raw-body "2.5.1"
type-is "~1.6.18"
unpipe "1.0.0"
body-parser@^1.20.2:
body-parser@1.20.2, body-parser@^1.20.2:
version "1.20.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
@ -2177,10 +2159,10 @@ cookie@0.4.2, cookie@~0.4.1:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
cookie@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
cookie@0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
copy-webpack-plugin@^10.2.4:
version "10.2.4"
@ -2855,16 +2837,16 @@ express-session@^1.17.3:
uid-safe "~2.1.5"
express@^4.18.2:
version "4.18.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
version "4.19.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "1.20.1"
body-parser "1.20.2"
content-disposition "0.5.4"
content-type "~1.0.4"
cookie "0.5.0"
cookie "0.6.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
@ -4664,20 +4646,20 @@ range-parser@~1.2.0, range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.5.1, raw-body@^2.3.2:
version "2.5.1"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
raw-body@2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.4.24"
unpipe "1.0.0"
raw-body@2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
raw-body@^2.3.2:
version "2.5.1"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"

Loading…
Cancel
Save