Browse Source

Cldc 1668 deactivate location (#981)

* add deactivation_date to locations

* Change conditional question controller to accommodate all models

* UI spike

* Update toggle-active views and render them correctly

* Display 2 errors

* Update errors

* Extract text to translation file

* Add collection start date

* Add out of range validation

* Update affected logs label

* lint

* Add status method

* update the displayed status tags

* Keep deactivation_date_type selected if an error occurs

* refactor deactivation_date_type to use default and other as options instead of 1 and 2

* refactor

* refactor

* update lettings logs

* Add reactivate ocation button and path

* Fix controller and update deactivate confirm page

* Don't actually update the logs data when deactivating a location

* lint and typos

* update a path

* update current_collection_start_date

* Remove unused scope
pull/986/head
kosiakkatrina 2 years ago committed by GitHub
parent
commit
1630c38619
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 65
      app/controllers/locations_controller.rb
  2. 6
      app/frontend/controllers/conditional_question_controller.js
  3. 10
      app/helpers/question_attribute_helper.rb
  4. 4
      app/helpers/tag_helper.rb
  5. 4
      app/models/form_handler.rb
  6. 7
      app/models/location.rb
  7. 6
      app/views/locations/show.html.erb
  8. 39
      app/views/locations/toggle_active.html.erb
  9. 16
      app/views/locations/toggle_active_confirm.html.erb
  10. 16
      config/locales/en.yml
  11. 2
      config/routes.rb
  12. 5
      db/migrate/20221109122033_add_deactivation_date_to_locations.rb
  13. 3
      db/schema.rb
  14. 2
      spec/helpers/locations_helper_spec.rb
  15. 2
      spec/helpers/question_attribute_helper_spec.rb
  16. 4
      spec/models/form_handler_spec.rb
  17. 32
      spec/models/location_spec.rb
  18. 185
      spec/requests/locations_controller_spec.rb

65
app/controllers/locations_controller.rb

@ -21,7 +21,23 @@ class LocationsController < ApplicationController
def show; end
def deactivate
render "toggle_active", locals: { action: "deactivate" }
if params[:location].blank?
render "toggle_active", locals: { action: "deactivate" }
elsif params[:location][:confirm].present? && params[:location][:deactivation_date].present?
confirm_deactivation
else
deactivation_date_errors
if @location.errors.present?
@location.deactivation_date_type = params[:location][:deactivation_date_type]
render "toggle_active", locals: { action: "deactivate" }, status: :unprocessable_entity
else
render "toggle_active_confirm", locals: { action: "deactivate", deactivation_date: }
end
end
end
def reactivate
render "toggle_active", locals: { action: "reactivate" }
end
def create
@ -128,7 +144,7 @@ private
end
def authenticate_action!
if %w[new edit update create index edit_name edit_local_authority].include?(action_name) && !((current_user.organisation == @scheme&.owning_organisation) || current_user.support?)
if %w[new edit update create index edit_name edit_local_authority deactivate].include?(action_name) && !((current_user.organisation == @scheme&.owning_organisation) || current_user.support?)
render_not_found and return
end
end
@ -146,4 +162,49 @@ private
def valid_location_admin_district?(location_params)
location_params["location_admin_district"] != "Select an option"
end
def confirm_deactivation
if @location.update(deactivation_date: params[:location][:deactivation_date])
flash[:notice] = "#{@location.name || @location.postcode} has been deactivated"
end
redirect_to scheme_location_path(@scheme, @location)
end
def deactivation_date_errors
if params[:location][:deactivation_date].blank? && params[:location][:deactivation_date_type].blank?
@location.errors.add(:deactivation_date_type, message: I18n.t("validations.location.deactivation_date.not_selected"))
end
if params[:location][:deactivation_date_type] == "other"
day = params[:location]["deactivation_date(3i)"]
month = params[:location]["deactivation_date(2i)"]
year = params[:location]["deactivation_date(1i)"]
collection_start_date = FormHandler.instance.current_collection_start_date
if [day, month, year].any?(&:blank?)
{ day:, month:, year: }.each do |period, value|
@location.errors.add(:deactivation_date, message: I18n.t("validations.location.deactivation_date.not_entered", period: period.to_s)) if value.blank?
end
elsif !Date.valid_date?(year.to_i, month.to_i, day.to_i)
@location.errors.add(:deactivation_date, message: I18n.t("validations.location.deactivation_date.invalid"))
elsif !Date.new(year.to_i, month.to_i, day.to_i).between?(collection_start_date, Date.new(2200, 1, 1))
@location.errors.add(:deactivation_date, message: I18n.t("validations.location.deactivation_date.out_of_range", date: collection_start_date.to_formatted_s(:govuk_date)))
end
end
end
def deactivation_date
return if params[:location].blank?
collection_start_date = FormHandler.instance.current_collection_start_date
return collection_start_date if params[:location][:deactivation_date_type] == "default"
return params[:location][:deactivation_date] if params[:location][:deactivation_date_type].blank?
day = params[:location]["deactivation_date(3i)"]
month = params[:location]["deactivation_date(2i)"]
year = params[:location]["deactivation_date(1i)"]
Date.new(year.to_i, month.to_i, day.to_i)
end
end

6
app/frontend/controllers/conditional_question_controller.js

@ -10,14 +10,14 @@ export default class extends Controller {
const selectedValue = this.element.value
const dataInfo = JSON.parse(this.element.dataset.info)
const conditionalFor = dataInfo.conditional_questions
const logType = dataInfo.log_type
const type = dataInfo.type
Object.entries(conditionalFor).forEach(([targetQuestion, conditions]) => {
if (!conditions.map(String).includes(String(selectedValue))) {
const textNumericInput = document.getElementById(`${logType}-log-${targetQuestion.replaceAll('_', '-')}-field`)
const textNumericInput = document.getElementById(`${type}-${targetQuestion.replaceAll('_', '-')}-field`)
if (textNumericInput == null) {
const dateInputs = [1, 2, 3].map((idx) => {
return document.getElementById(`${logType}_log_${targetQuestion}_${idx}i`)
return document.getElementById(`${type.replaceAll('-', '_')}_${targetQuestion}_${idx}i`)
})
this.clearDateInputs(dateInputs)
} else {

10
app/helpers/question_attribute_helper.rb

@ -7,6 +7,14 @@ module QuestionAttributeHelper
merge_controller_attributes(*attribs)
end
def basic_conditional_html_attributes(conditional_for, type)
{
"data-controller": "conditional-question",
"data-action": "click->conditional-question#displayConditional",
"data-info": { conditional_questions: conditional_for, type: type }.to_json,
}
end
private
def numeric_question_html_attributes(question)
@ -27,7 +35,7 @@ private
{
"data-controller": "conditional-question",
"data-action": "click->conditional-question#displayConditional",
"data-info": { conditional_questions: question.conditional_for, log_type: question.form.type }.to_json,
"data-info": { conditional_questions: question.conditional_for, type: "#{question.form.type}-log" }.to_json,
}
end
end

4
app/helpers/tag_helper.rb

@ -7,6 +7,8 @@ module TagHelper
in_progress: "In progress",
completed: "Completed",
active: "Active",
deactivating_soon: "Deactivating soon",
deactivated: "Deactivated",
}.freeze
COLOUR = {
@ -15,6 +17,8 @@ module TagHelper
in_progress: "blue",
completed: "green",
active: "green",
deactivating_soon: "yellow",
deactivated: "grey",
}.freeze
def status_tag(status, classes = [])

4
app/models/form_handler.rb

@ -49,6 +49,10 @@ class FormHandler
today < window_end_date ? today.year - 1 : today.year
end
def current_collection_start_date
Time.utc(current_collection_start_year, 4, 1)
end
def form_name_from_start_year(year, type)
form_mappings = { 0 => "current_#{type}", 1 => "previous_#{type}", -1 => "next_#{type}" }
form_mappings[current_collection_start_year - year]

7
app/models/location.rb

@ -2,6 +2,7 @@ class Location < ApplicationRecord
validate :validate_postcode
validates :units, :type_of_unit, :mobility_type, presence: true
belongs_to :scheme
has_many :lettings_logs, class_name: "LettingsLog"
has_paper_trail
@ -9,7 +10,7 @@ class Location < ApplicationRecord
auto_strip_attributes :name
attr_accessor :add_another_location
attr_accessor :add_another_location, :deactivation_date_type
scope :search_by_postcode, ->(postcode) { where("REPLACE(postcode, ' ', '') ILIKE ?", "%#{postcode.delete(' ')}%") }
scope :search_by_name, ->(name) { where("name ILIKE ?", "%#{name}%") }
@ -372,7 +373,9 @@ class Location < ApplicationRecord
end
def status
"active"
return :active if deactivation_date.blank?
return :deactivating_soon if Time.zone.now < deactivation_date
return :deactivated if Time.zone.now >= deactivation_date
end
private

6
app/views/locations/show.html.erb

@ -24,5 +24,9 @@
</div>
</div>
<% if FeatureToggle.location_toggle_enabled? %>
<%= govuk_button_link_to "Deactivate this location", scheme_location_deactivate_path(scheme_id: @scheme.id, location_id: @location.id), warning: true %>
<% if @location.status == :active %>
<%= govuk_button_link_to "Deactivate this location", scheme_location_deactivate_path(scheme_id: @scheme.id, location_id: @location.id), warning: true %>
<% else %>
<%= govuk_button_link_to "Reactivate this location", scheme_location_reactivate_path(scheme_id: @scheme.id, location_id: @location.id) %>
<% end %>
<% end %>

39
app/views/locations/toggle_active.html.erb

@ -0,0 +1,39 @@
<% title = "#{action.humanize} #{@location.postcode}" %>
<% content_for :title, title %>
<% content_for :before_content do %>
<%= govuk_back_link(
text: "Back",
href: scheme_location_path(scheme_id: @location.scheme.id, id: @location.id),
) %>
<% end %>
<%= form_with model: @location, url: scheme_location_deactivate_path(scheme_id: @location.scheme.id, location_id: @location.id), method: "patch", local: true do |f| %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<% collection_start_date = FormHandler.instance.current_collection_start_date %>
<%= f.govuk_error_summary %>
<%= f.govuk_radio_buttons_fieldset :deactivation_date_type,
legend: { text: I18n.t("questions.location.deactivation.apply_from") },
caption: { text: "Deactivate #{@location.postcode}" },
hint: { text: I18n.t("hints.location.deactivation", date: collection_start_date.to_formatted_s(:govuk_date)) } do %>
<%= govuk_warning_text text: I18n.t("warnings.location.deactivation.existing_logs") %>
<%= f.govuk_radio_button :deactivation_date_type,
"default",
label: { text: "From the start of the current collection period (#{collection_start_date.to_formatted_s(:govuk_date)})" } %>
<%= f.govuk_radio_button :deactivation_date_type,
"other",
label: { text: "For tenancies starting after a certain date" },
**basic_conditional_html_attributes({ "deactivation_date" => %w[other] }, "location") do %>
<%= f.govuk_date_field :deactivation_date,
legend: { text: "Date", size: "m" },
hint: { text: "For example, 27 3 2008" },
width: 20 %>
<% end %>
<% end %>
<%= f.govuk_submit "Continue" %>
</div>
</div>
<% end %>

16
app/views/locations/toggle_active_confirm.html.erb

@ -0,0 +1,16 @@
<%= form_with model: @location, url: scheme_location_deactivate_path(@location), method: "patch", local: true do |f| %>
<% content_for :before_content do %>
<%= govuk_back_link(href: :back) %>
<% end %>
<h1 class="govuk-heading-l">
<span class="govuk-caption-l"><%= @location.postcode %></span>
<%= "This change will affect #{@location.lettings_logs.count} logs" %>
</h1>
<%= govuk_warning_text text: I18n.t("warnings.location.deactivation.review_logs") %>
<%= f.hidden_field :confirm, value: true %>
<%= f.hidden_field :deactivation_date, value: deactivation_date %>
<div class="govuk-button-group">
<%= f.govuk_submit "Deactivate this location" %>
<%= govuk_button_link_to "Cancel", scheme_location_path(scheme_id: @scheme, id: @location.id), html: { method: :get }, secondary: true %>
</div>
<% end %>

16
config/locales/en.yml

@ -312,6 +312,13 @@ en:
declaration:
missing: "You must show the DLUHC privacy notice to the tenant before you can submit this log."
location:
deactivation_date:
not_selected: "Select one of the options"
not_entered: "Enter a %{period}"
invalid: "Enter a valid date"
out_of_range: "The date must be on or after the %{date}"
soft_validations:
net_income:
title_text: "Net income is outside the expected range based on the lead tenant’s working situation"
@ -362,6 +369,8 @@ en:
startdate: "When did the first property in this location become available under this scheme? (optional)"
add_another_location: "Do you want to add another location?"
mobility_type: "What are the mobility standards for the majority of units in this location?"
deactivation:
apply_from: "When should this change apply?"
descriptions:
location:
mobility_type:
@ -374,6 +383,13 @@ en:
postcode: "For example, SW1P 4DF."
name: "This is how you refer to this location within your organisation"
units: "A unit can be a bedroom in a shared house or flat, or a house with 4 bedrooms. Do not include bedrooms used for wardens, managers, volunteers or sleep-in staff."
deactivation: "If the date is before %{date}, select ‘From the start of the current collection period’ because the previous period has now closed."
warnings:
location:
deactivation:
existing_logs: "It will not be possible to add logs with this location if their tenancy start date is on or after the date you enter. Any existing logs may be affected."
review_logs: "Your data providers will need to review these logs and answer a few questions again. We’ll email each log creator with a list of logs that need updating."
test:
one_argument: "This is based on the tenant’s work situation: %{ecstat1}"

2
config/routes.rb

@ -54,6 +54,8 @@ Rails.application.routes.draw do
get "edit-name", to: "locations#edit_name"
get "edit-local-authority", to: "locations#edit_local_authority"
get "deactivate", to: "locations#deactivate"
get "reactivate", to: "locations#reactivate"
patch "deactivate", to: "locations#deactivate"
end
end

5
db/migrate/20221109122033_add_deactivation_date_to_locations.rb

@ -0,0 +1,5 @@
class AddDeactivationDateToLocations < ActiveRecord::Migration[7.0]
def change
add_column :locations, :deactivation_date, :datetime
end
end

3
db/schema.rb

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2022_10_19_082625) do
ActiveRecord::Schema[7.0].define(version: 2022_11_09_122033) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -260,6 +260,7 @@ ActiveRecord::Schema[7.0].define(version: 2022_10_19_082625) do
t.datetime "startdate", precision: nil
t.string "location_admin_district"
t.boolean "confirmed"
t.datetime "deactivation_date"
t.index ["old_id"], name: "index_locations_on_old_id", unique: true
t.index ["scheme_id"], name: "index_locations_on_scheme_id"
end

2
spec/helpers/locations_helper_spec.rb

@ -60,7 +60,7 @@ RSpec.describe LocationsHelper do
{ name: "Mobility type", value: location.mobility_type },
{ name: "Code", value: location.location_code },
{ name: "Availability", value: "Available from 8 August 2022" },
{ name: "Status", value: "active" },
{ name: "Status", value: :active },
]
expect(display_attributes(location)).to eq(attributes)

2
spec/helpers/question_attribute_helper_spec.rb

@ -48,7 +48,7 @@ RSpec.describe QuestionAttributeHelper do
"data-action": "input->numeric-question#calculateFields click->conditional-question#displayConditional",
"data-target": "lettings-log-#{question.result_field.to_s.dasherize}-field",
"data-calculated": question.fields_to_add.to_json,
"data-info": { conditional_questions: question.conditional_for, log_type: "lettings" }.to_json,
"data-info": { conditional_questions: question.conditional_for, type: "lettings-log" }.to_json,
}
end

4
spec/models/form_handler_spec.rb

@ -118,6 +118,10 @@ RSpec.describe FormHandler do
it "returns the correct next sales form name" do
expect(form_handler.form_name_from_start_year(2023, "sales")).to eq("next_sales")
end
it "returns the correct current start date" do
expect(form_handler.current_collection_start_date).to eq(Time.utc(2022, 4, 1))
end
end
context "with the date before 1st of April" do

32
spec/models/location_spec.rb

@ -111,4 +111,36 @@ RSpec.describe Location, type: :model do
end
end
end
describe "status" do
let(:location) { FactoryBot.build(:location) }
before do
Timecop.freeze(2022, 6, 7)
end
it "returns active if the location is not deactivated" do
location.deactivation_date = nil
location.save!
expect(location.status).to eq(:active)
end
it "returns deactivating soon if deactivation_date is in the future" do
location.deactivation_date = Time.zone.local(2022, 8, 8)
location.save!
expect(location.status).to eq(:deactivating_soon)
end
it "returns deactivated if deactivation_date is in the past" do
location.deactivation_date = Time.zone.local(2022, 4, 8)
location.save!
expect(location.status).to eq(:deactivated)
end
it "returns deactivated if deactivation_date is today" do
location.deactivation_date = Time.zone.local(2022, 6, 7)
location.save!
expect(location.status).to eq(:deactivated)
end
end
end

185
spec/requests/locations_controller_spec.rb

@ -1212,4 +1212,189 @@ RSpec.describe LocationsController, type: :request do
end
end
end
describe "#deactivate" do
context "when not signed in" do
it "redirects to the sign in page" do
patch "/schemes/1/locations/1/deactivate"
expect(response).to redirect_to("/account/sign-in")
end
end
context "when signed in as a data provider" do
let(:user) { FactoryBot.create(:user) }
before do
sign_in user
patch "/schemes/1/locations/1/deactivate"
end
it "returns 401 unauthorized" do
request
expect(response).to have_http_status(:unauthorized)
end
end
context "when signed in as a data coordinator" do
let(:user) { FactoryBot.create(:user, :data_coordinator) }
let!(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation) }
let!(:location) { FactoryBot.create(:location, scheme:) }
let(:startdate) { Time.utc(2021, 1, 2) }
let(:deactivation_date) { Time.utc(2022, 10, 10) }
before do
Timecop.freeze(Time.utc(2022, 10, 10))
sign_in user
patch "/schemes/#{scheme.id}/locations/#{location.id}/deactivate", params:
end
context "with default date" do
let(:params) { { location: { deactivation_date_type: "default" } } }
it "renders the confirmation page" do
expect(response).to have_http_status(:ok)
expect(page).to have_content("This change will affect #{location.lettings_logs.count} logs")
end
end
context "with other date" do
let(:params) { { location: { deactivation_date: "other", "deactivation_date(3i)": "10", "deactivation_date(2i)": "10", "deactivation_date(1i)": "2022" } } }
it "renders the confirmation page" do
expect(response).to have_http_status(:ok)
expect(page).to have_content("This change will affect #{location.lettings_logs.count} logs")
end
end
context "when confirming deactivation" do
let(:params) { { location: { deactivation_date:, confirm: true } } }
it "updates existing location with valid deactivation date and renders location page" do
follow_redirect!
expect(response).to have_http_status(:ok)
expect(page).to have_css(".govuk-notification-banner.govuk-notification-banner--success")
location.reload
expect(location.deactivation_date).to eq(deactivation_date)
end
end
context "when the date is not selected" do
let(:params) { { location: { "deactivation_date": "" } } }
it "displays the new page with an error message" do
expect(response).to have_http_status(:unprocessable_entity)
expect(page).to have_content(I18n.t("validations.location.deactivation_date.not_selected"))
end
end
context "when invalid date is entered" do
let(:params) { { location: { deactivation_date_type: "other", "deactivation_date(3i)": "10", "deactivation_date(2i)": "44", "deactivation_date(1i)": "2022" } } }
it "displays the new page with an error message" do
expect(response).to have_http_status(:unprocessable_entity)
expect(page).to have_content(I18n.t("validations.location.deactivation_date.invalid"))
end
end
context "when the date is entered is before the beginning of current collection window" do
let(:params) { { location: { deactivation_date_type: "other", "deactivation_date(3i)": "10", "deactivation_date(2i)": "4", "deactivation_date(1i)": "2020" } } }
it "displays the new page with an error message" do
expect(response).to have_http_status(:unprocessable_entity)
expect(page).to have_content(I18n.t("validations.location.deactivation_date.out_of_range", date: "1 April 2022"))
end
end
context "when the day is not entered" do
let(:params) { { location: { deactivation_date_type: "other", "deactivation_date(3i)": "", "deactivation_date(2i)": "2", "deactivation_date(1i)": "2022" } } }
it "displays page with an error message" do
expect(response).to have_http_status(:unprocessable_entity)
expect(page).to have_content(I18n.t("validations.location.deactivation_date.not_entered", period: "day"))
end
end
context "when the month is not entered" do
let(:params) { { location: { deactivation_date_type: "other", "deactivation_date(3i)": "2", "deactivation_date(2i)": "", "deactivation_date(1i)": "2022" } } }
it "displays page with an error message" do
expect(response).to have_http_status(:unprocessable_entity)
expect(page).to have_content(I18n.t("validations.location.deactivation_date.not_entered", period: "month"))
end
end
context "when the year is not entered" do
let(:params) { { location: { deactivation_date_type: "other", "deactivation_date(3i)": "2", "deactivation_date(2i)": "2", "deactivation_date(1i)": "" } } }
it "displays page with an error message" do
expect(response).to have_http_status(:unprocessable_entity)
expect(page).to have_content(I18n.t("validations.location.deactivation_date.not_entered", period: "year"))
end
end
end
end
describe "#show" do
context "when not signed in" do
it "redirects to the sign in page" do
get "/schemes/1/locations/1"
expect(response).to redirect_to("/account/sign-in")
end
end
context "when signed in as a data provider" do
let(:user) { FactoryBot.create(:user) }
before do
sign_in user
get "/schemes/1/locations/1"
end
it "returns 401 unauthorized" do
request
expect(response).to have_http_status(:unauthorized)
end
end
context "when signed in as a data coordinator" do
let(:user) { FactoryBot.create(:user, :data_coordinator) }
let!(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation) }
let!(:location) { FactoryBot.create(:location, scheme:) }
before do
Timecop.freeze(Time.utc(2022, 10, 10))
sign_in user
location.deactivation_date = deactivation_date
location.save!
get "/schemes/#{scheme.id}/locations/#{location.id}"
end
context "with active location" do
let(:deactivation_date) { nil }
it "renders deactivate this location" do
expect(response).to have_http_status(:ok)
expect(page).to have_link("Deactivate this location", href: "/schemes/#{scheme.id}/locations/#{location.id}/deactivate")
end
end
context "with deactivated location" do
let(:deactivation_date) { Time.utc(2022, 10, 9) }
it "renders reactivate this location" do
expect(response).to have_http_status(:ok)
expect(page).to have_link("Reactivate this location", href: "/schemes/#{scheme.id}/locations/#{location.id}/reactivate")
end
end
context "with location that's deactivating soon" do
let(:deactivation_date) { Time.utc(2022, 10, 12) }
it "renders reactivate this location" do
expect(response).to have_http_status(:ok)
expect(page).to have_link("Reactivate this location", href: "/schemes/#{scheme.id}/locations/#{location.id}/reactivate")
end
end
end
end
end

Loading…
Cancel
Save