diff --git a/.rubocop.yml b/.rubocop.yml
index 9e252d614..f47dd6fb6 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -335,6 +335,7 @@ Rails/SkipsModelValidations:
ForbiddenMethods:
- update_attribute
Exclude:
+ - app/models/tenant.rb
- lib/acts_as_paranoid_aliases.rb
Rails/TimeZone:
diff --git a/app/assets/stylesheets/admin/budget_phases/phases.scss b/app/assets/stylesheets/admin/budget_phases/phases.scss
index d627cf055..7d84f5231 100644
--- a/app/assets/stylesheets/admin/budget_phases/phases.scss
+++ b/app/assets/stylesheets/admin/budget_phases/phases.scss
@@ -3,9 +3,4 @@
caption {
@include element-invisible;
}
-
- [aria-pressed] {
- @include switch;
- margin-bottom: 0;
- }
}
diff --git a/app/assets/stylesheets/admin/toggle_switch.scss b/app/assets/stylesheets/admin/toggle_switch.scss
new file mode 100644
index 000000000..501e85eaa
--- /dev/null
+++ b/app/assets/stylesheets/admin/toggle_switch.scss
@@ -0,0 +1,6 @@
+.admin .toggle-switch {
+ [aria-pressed] {
+ @include switch;
+ margin-bottom: 0;
+ }
+}
diff --git a/app/components/admin/budget_phases/toggle_enabled_component.html.erb b/app/components/admin/budget_phases/toggle_enabled_component.html.erb
index d230e97e5..832049577 100644
--- a/app/components/admin/budget_phases/toggle_enabled_component.html.erb
+++ b/app/components/admin/budget_phases/toggle_enabled_component.html.erb
@@ -1 +1 @@
-<%= render Admin::ActionComponent.new(:toggle_enabled, phase, options) %>
+<%= render Admin::ToggleSwitchComponent.new(action, phase, pressed: enabled?, **options) %>
diff --git a/app/components/admin/budget_phases/toggle_enabled_component.rb b/app/components/admin/budget_phases/toggle_enabled_component.rb
index 0236cdae5..daf2693b1 100644
--- a/app/components/admin/budget_phases/toggle_enabled_component.rb
+++ b/app/components/admin/budget_phases/toggle_enabled_component.rb
@@ -1,5 +1,6 @@
class Admin::BudgetPhases::ToggleEnabledComponent < ApplicationComponent
attr_reader :phase
+ delegate :enabled?, to: :phase
def initialize(phase)
@phase = phase
@@ -8,20 +9,14 @@ class Admin::BudgetPhases::ToggleEnabledComponent < ApplicationComponent
private
def options
- {
- text: text,
- method: :patch,
- remote: true,
- "aria-label": t("admin.budgets.edit.enable_phase", phase: phase.name),
- "aria-pressed": phase.enabled?
- }
+ { "aria-label": t("admin.budgets.edit.enable_phase", phase: phase.name) }
end
- def text
- if phase.enabled?
- t("shared.yes")
+ def action
+ if enabled?
+ :disable
else
- t("shared.no")
+ :enable
end
end
end
diff --git a/app/components/admin/tenants/index_component.html.erb b/app/components/admin/tenants/index_component.html.erb
index ad5f43ea9..63bb3504e 100644
--- a/app/components/admin/tenants/index_component.html.erb
+++ b/app/components/admin/tenants/index_component.html.erb
@@ -8,20 +8,14 @@
<%= attribute_name(:name) %> |
<%= attribute_name(:schema) %> |
<%= attribute_name(:url) %> |
+ <%= t("admin.tenants.index.enabled") %> |
<%= t("admin.shared.actions") %> |
<% @tenants.each do |tenant| %>
-
- | <%= tenant.name %> |
- <%= tenant.schema %> |
- <%= link_to tenant.host, root_url(host: tenant.host) %> |
-
- <%= render Admin::TableActionsComponent.new(tenant, actions: [:edit]) %>
- |
-
+ <%= render Admin::Tenants::RowComponent.new(tenant) %>
<% end %>
diff --git a/app/components/admin/tenants/row_component.html.erb b/app/components/admin/tenants/row_component.html.erb
new file mode 100644
index 000000000..365866676
--- /dev/null
+++ b/app/components/admin/tenants/row_component.html.erb
@@ -0,0 +1,9 @@
+
+ | <%= tenant.name %> |
+ <%= tenant.schema %> |
+ <%= link_to_unless tenant.hidden?, tenant.host, root_url(host: tenant.host) %> |
+ <%= render Admin::Tenants::ToggleHiddenComponent.new(tenant) %> |
+
+ <%= render Admin::TableActionsComponent.new(tenant, actions: [:edit]) %>
+ |
+
diff --git a/app/components/admin/tenants/row_component.rb b/app/components/admin/tenants/row_component.rb
new file mode 100644
index 000000000..648b0db7a
--- /dev/null
+++ b/app/components/admin/tenants/row_component.rb
@@ -0,0 +1,7 @@
+class Admin::Tenants::RowComponent < ApplicationComponent
+ attr_reader :tenant
+
+ def initialize(tenant)
+ @tenant = tenant
+ end
+end
diff --git a/app/components/admin/tenants/toggle_hidden_component.html.erb b/app/components/admin/tenants/toggle_hidden_component.html.erb
new file mode 100644
index 000000000..64a3a2e93
--- /dev/null
+++ b/app/components/admin/tenants/toggle_hidden_component.html.erb
@@ -0,0 +1 @@
+<%= render Admin::ToggleSwitchComponent.new(action, tenant, pressed: enabled?, **options) %>
diff --git a/app/components/admin/tenants/toggle_hidden_component.rb b/app/components/admin/tenants/toggle_hidden_component.rb
new file mode 100644
index 000000000..12488ce21
--- /dev/null
+++ b/app/components/admin/tenants/toggle_hidden_component.rb
@@ -0,0 +1,28 @@
+class Admin::Tenants::ToggleHiddenComponent < ApplicationComponent
+ attr_reader :tenant
+
+ def initialize(tenant)
+ @tenant = tenant
+ end
+
+ private
+
+ def action
+ if enabled?
+ :hide
+ else
+ :restore
+ end
+ end
+
+ def options
+ {
+ method: :put,
+ "aria-label": t("admin.tenants.index.enable", tenant: tenant.name)
+ }
+ end
+
+ def enabled?
+ !tenant.hidden?
+ end
+end
diff --git a/app/components/admin/toggle_switch_component.html.erb b/app/components/admin/toggle_switch_component.html.erb
new file mode 100644
index 000000000..35b566c48
--- /dev/null
+++ b/app/components/admin/toggle_switch_component.html.erb
@@ -0,0 +1 @@
+<%= render Admin::ActionComponent.new(action, record, **default_options.merge(options)) %>
diff --git a/app/components/admin/toggle_switch_component.rb b/app/components/admin/toggle_switch_component.rb
new file mode 100644
index 000000000..ceee74d5b
--- /dev/null
+++ b/app/components/admin/toggle_switch_component.rb
@@ -0,0 +1,31 @@
+class Admin::ToggleSwitchComponent < ApplicationComponent
+ attr_reader :action, :record, :pressed, :options
+ alias_method :pressed?, :pressed
+
+ def initialize(action, record, pressed:, **options)
+ @action = action
+ @record = record
+ @pressed = pressed
+ @options = options
+ end
+
+ private
+
+ def text
+ if pressed?
+ t("shared.yes")
+ else
+ t("shared.no")
+ end
+ end
+
+ def default_options
+ {
+ text: text,
+ method: :patch,
+ remote: true,
+ "aria-pressed": pressed?,
+ form_class: "toggle-switch"
+ }
+ end
+end
diff --git a/app/controllers/admin/tenants_controller.rb b/app/controllers/admin/tenants_controller.rb
index a43e88166..e7d6e949a 100644
--- a/app/controllers/admin/tenants_controller.rb
+++ b/app/controllers/admin/tenants_controller.rb
@@ -27,6 +27,24 @@ class Admin::TenantsController < Admin::BaseController
end
end
+ def hide
+ @tenant.hide
+
+ respond_to do |format|
+ format.html { redirect_to admin_tenants_path, notice: t("admin.tenants.hide.notice") }
+ format.js { render template: "admin/tenants/toggle_enabled" }
+ end
+ end
+
+ def restore
+ @tenant.restore
+
+ respond_to do |format|
+ format.html { redirect_to admin_tenants_path, notice: t("admin.tenants.restore.notice") }
+ format.js { render template: "admin/tenants/toggle_enabled" }
+ end
+ end
+
private
def tenant_params
diff --git a/app/controllers/concerns/admin/budget_phases_actions.rb b/app/controllers/concerns/admin/budget_phases_actions.rb
index 8c267c364..ff38f196a 100644
--- a/app/controllers/concerns/admin/budget_phases_actions.rb
+++ b/app/controllers/concerns/admin/budget_phases_actions.rb
@@ -6,7 +6,7 @@ module Admin::BudgetPhasesActions
include ImageAttributes
before_action :load_budget
- before_action :load_phase, only: [:edit, :update, :toggle_enabled]
+ before_action :load_phase, only: [:edit, :update, :enable, :disable]
end
def edit
@@ -20,12 +20,21 @@ module Admin::BudgetPhasesActions
end
end
- def toggle_enabled
- @phase.update!(enabled: !@phase.enabled)
+ def enable
+ @phase.update!(enabled: true)
respond_to do |format|
format.html { redirect_to phases_index, notice: t("flash.actions.save_changes.notice") }
- format.js
+ format.js { render template: "admin/budgets_wizard/phases/toggle_enabled" }
+ end
+ end
+
+ def disable
+ @phase.update!(enabled: false)
+
+ respond_to do |format|
+ format.html { redirect_to phases_index, notice: t("flash.actions.save_changes.notice") }
+ format.js { render template: "admin/budgets_wizard/phases/toggle_enabled" }
end
end
diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb
index 9e3556e80..f874af58b 100644
--- a/app/models/abilities/administrator.rb
+++ b/app/models/abilities/administrator.rb
@@ -136,7 +136,7 @@ module Abilities
can [:create, :read], LocalCensusRecords::Import
if Rails.application.config.multitenancy && Tenant.default?
- can [:create, :read, :update], Tenant
+ can [:create, :read, :update, :hide, :restore], Tenant
end
end
end
diff --git a/app/models/tenant.rb b/app/models/tenant.rb
index 54a254c39..654cbecf0 100644
--- a/app/models/tenant.rb
+++ b/app/models/tenant.rb
@@ -12,6 +12,8 @@ class Tenant < ApplicationRecord
after_update :rename_schema
after_destroy :destroy_schema
+ scope :only_hidden, -> { where.not(hidden_at: nil) }
+
def self.find_by_domain(host)
domain.find_by(schema: host)
end
@@ -20,6 +22,16 @@ class Tenant < ApplicationRecord
return nil unless Rails.application.config.multitenancy.present?
return nil if host.blank? || host.match?(Resolv::AddressRegex)
+ schema = schema_for(host)
+
+ if schema && only_hidden.find_by(schema: schema)
+ raise Apartment::TenantNotFound
+ else
+ schema
+ end
+ end
+
+ def self.schema_for(host)
host_without_www = host.delete_prefix("www.")
if find_by_domain(host)
@@ -30,7 +42,11 @@ class Tenant < ApplicationRecord
host_domain = allowed_domains.find { |domain| host == domain || host.ends_with?(".#{domain}") }
schema = host_without_www.sub(/\.?#{host_domain}\Z/, "").presence
- schema unless find_by_domain(schema)
+ if find_by_domain(schema)
+ raise Apartment::TenantNotFound
+ else
+ schema
+ end
end
end
@@ -133,6 +149,18 @@ class Tenant < ApplicationRecord
self.class.host_for(schema)
end
+ def hide
+ update_attribute(:hidden_at, Time.current)
+ end
+
+ def restore
+ update_attribute(:hidden_at, nil)
+ end
+
+ def hidden?
+ hidden_at.present?
+ end
+
private
def create_schema
diff --git a/app/views/admin/budget_phases/toggle_enabled.js.erb b/app/views/admin/budget_phases/toggle_enabled.js.erb
deleted file mode 100644
index b90a979bd..000000000
--- a/app/views/admin/budget_phases/toggle_enabled.js.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= render template: "admin/budgets_wizard/phases/toggle_enabled" %>
diff --git a/app/views/admin/budgets_wizard/phases/toggle_enabled.js.erb b/app/views/admin/budgets_wizard/phases/toggle_enabled.js.erb
index d4424bf4c..d0688ab25 100644
--- a/app/views/admin/budgets_wizard/phases/toggle_enabled.js.erb
+++ b/app/views/admin/budgets_wizard/phases/toggle_enabled.js.erb
@@ -1,4 +1,5 @@
var replacement = $("<%= j render Admin::BudgetPhases::ToggleEnabledComponent.new(@phase) %>");
-var form = $("#" + replacement.find("[type='submit']").attr("id")).closest("form");
+var form = $("#<%= dom_id(@phase) %> .toggle-switch");
-form.html(replacement.html()).find("[type='submit']").focus();
+form.replaceWith(replacement);
+replacement.find("[type='submit']").focus();
diff --git a/app/views/admin/tenants/toggle_enabled.js.erb b/app/views/admin/tenants/toggle_enabled.js.erb
new file mode 100644
index 000000000..8e2a0eadc
--- /dev/null
+++ b/app/views/admin/tenants/toggle_enabled.js.erb
@@ -0,0 +1,4 @@
+var replacement = $("<%= j render Admin::Tenants::RowComponent.new(@tenant) %>");
+var row = $("#<%= dom_id(@tenant) %>");
+
+row.html(replacement.html()).find(".toggle-switch [type='submit']").focus();
diff --git a/config/locales/en/admin.yml b/config/locales/en/admin.yml
index 4f59150ad..b128ec8cf 100644
--- a/config/locales/en/admin.yml
+++ b/config/locales/en/admin.yml
@@ -1635,10 +1635,16 @@ en:
form:
use_subdomain: "Use a subdomain in the %{domain} domain to access this tenant"
use_domain: "Use a different domain to access this tenant"
+ hide:
+ notice: Tenant disabled successfully
index:
create: Create tenant
+ enable: "Enable tenant %{tenant}"
+ enabled: Enabled
new:
title: New tenant
+ restore:
+ notice: Tenant enabled successfully
update:
notice: Tenant updated successfully
homepage:
diff --git a/config/locales/es/admin.yml b/config/locales/es/admin.yml
index 948c6cf5c..509748e57 100644
--- a/config/locales/es/admin.yml
+++ b/config/locales/es/admin.yml
@@ -1634,10 +1634,16 @@ es:
form:
use_subdomain: "Utiliza un subdominio en el dominio %{domain} para acceder a esta entidad"
use_domain: "Utiliza un dominio distinto para acceder a esta entidad"
+ hide:
+ notice: Entidad deshabilitada correctamente
index:
create: Crear entidad
+ enable: "Habilitar entidad %{tenant}"
+ enabled: Habilitada
new:
title: Nueva entidad
+ restore:
+ notice: Entidad habilitada correctamente
update:
notice: Entidad actualizada correctamente
homepage:
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 752942b82..2fa509e36 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -70,7 +70,10 @@ namespace :admin do
end
resources :budget_phases, only: [:edit, :update] do
- member { patch :toggle_enabled }
+ member do
+ patch :enable
+ patch :disable
+ end
end
end
@@ -81,7 +84,10 @@ namespace :admin do
end
resources :phases, as: "budget_phases", only: [:index, :edit, :update] do
- member { patch :toggle_enabled }
+ member do
+ patch :enable
+ patch :disable
+ end
end
end
end
@@ -278,7 +284,12 @@ namespace :admin do
delete :cancel, on: :collection
end
- resources :tenants, except: [:show, :destroy]
+ resources :tenants, except: [:show, :destroy] do
+ member do
+ put :hide
+ put :restore
+ end
+ end
end
resolve "Milestone" do |milestone|
diff --git a/db/migrate/20221203140136_add_hidden_at_to_tenants.rb b/db/migrate/20221203140136_add_hidden_at_to_tenants.rb
new file mode 100644
index 000000000..5e2006313
--- /dev/null
+++ b/db/migrate/20221203140136_add_hidden_at_to_tenants.rb
@@ -0,0 +1,5 @@
+class AddHiddenAtToTenants < ActiveRecord::Migration[6.0]
+ def change
+ add_column :tenants, :hidden_at, :datetime
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f12f53061..be20fc451 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2022_11_20_123254) do
+ActiveRecord::Schema.define(version: 2022_12_03_140136) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -1562,6 +1562,7 @@ ActiveRecord::Schema.define(version: 2022_11_20_123254) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "schema_type", default: 0, null: false
+ t.datetime "hidden_at"
t.index ["name"], name: "index_tenants_on_name", unique: true
t.index ["schema"], name: "index_tenants_on_schema", unique: true
end
diff --git a/spec/models/abilities/administrator_spec.rb b/spec/models/abilities/administrator_spec.rb
index a34ec08e6..956fed48c 100644
--- a/spec/models/abilities/administrator_spec.rb
+++ b/spec/models/abilities/administrator_spec.rb
@@ -181,6 +181,8 @@ describe Abilities::Administrator do
it { should be_able_to :create, Tenant }
it { should be_able_to :read, Tenant }
it { should be_able_to :update, Tenant }
+ it { should be_able_to :hide, Tenant }
+ it { should be_able_to :restore, Tenant }
it { should_not be_able_to :destroy, Tenant }
context "administrators from other tenants" do
@@ -193,6 +195,8 @@ describe Abilities::Administrator do
it { should_not be_able_to :read, Tenant }
it { should_not be_able_to :update, Tenant }
it { should_not be_able_to :destroy, Tenant }
+ it { should_not be_able_to :hide, Tenant }
+ it { should_not be_able_to :restore, Tenant }
end
end
end
diff --git a/spec/models/tenant_spec.rb b/spec/models/tenant_spec.rb
index e6d4bae67..ba94f001a 100644
--- a/spec/models/tenant_spec.rb
+++ b/spec/models/tenant_spec.rb
@@ -86,10 +86,10 @@ describe Tenant do
expect(Tenant.resolve_host("saturn.consul.dev")).to eq "saturn"
end
- it "returns nil when a domain is accessed as a subdomain" do
+ it "raises an exception when a domain is accessed as a subdomain" do
insert(:tenant, :domain, schema: "saturn.dev")
- expect(Tenant.resolve_host("saturn.dev.consul.dev")).to be nil
+ expect { Tenant.resolve_host("saturn.dev.consul.dev") }.to raise_exception(Apartment::TenantNotFound)
end
it "returns nested subdomains when there's a subdomain-type tenant with nested subdomains" do
@@ -117,6 +117,37 @@ describe Tenant do
expect(Tenant.resolve_host("www.consul.dev")).to eq "www.consul.dev"
end
+ it "raises an exception when accessing a hidden tenant using a subdomain" do
+ insert(:tenant, schema: "saturn", hidden_at: Time.current)
+
+ expect { Tenant.resolve_host("saturn.consul.dev") }.to raise_exception(Apartment::TenantNotFound)
+ end
+
+ it "raises an exception when accessing a hidden tenant using a domain" do
+ insert(:tenant, :domain, schema: "consul.dev", hidden_at: Time.current)
+
+ expect { Tenant.resolve_host("consul.dev") }.to raise_exception(Apartment::TenantNotFound)
+ end
+
+ it "raises an exception when accessing a hidden tenant using a domain starting with www" do
+ insert(:tenant, :domain, schema: "www.consul.dev", hidden_at: Time.current)
+
+ expect { Tenant.resolve_host("www.consul.dev") }.to raise_exception(Apartment::TenantNotFound)
+ end
+
+ it "raises an exception when accessing a hidden tenant with a domain and another tenant resolves to the same domain" do
+ insert(:tenant, :domain, schema: "saturn.consul.dev", hidden_at: Time.current)
+ insert(:tenant, schema: "saturn")
+
+ expect { Tenant.resolve_host("saturn.consul.dev") }.to raise_exception(Apartment::TenantNotFound)
+ end
+
+ it "ignores hidden tenants with nil as their schema" do
+ insert(:tenant, schema: nil, hidden_at: Time.current)
+
+ expect(Tenant.resolve_host("consul.dev")).to be nil
+ end
+
context "multitenancy disabled" do
before { allow(Rails.application.config).to receive(:multitenancy).and_return(false) }
@@ -318,6 +349,11 @@ describe Tenant do
expect(build(:tenant, schema: "subdomainx")).not_to be_valid
end
+ it "is not valid with the schema of an already existing hidden record" do
+ insert(:tenant, schema: "subdomainx", hidden_at: Time.current)
+ expect(build(:tenant, schema: "subdomainx")).not_to be_valid
+ end
+
it "is not valid with an excluded subdomain" do
%w[mail public shared_extensions www].each do |subdomain|
tenant.schema = subdomain
@@ -345,6 +381,11 @@ describe Tenant do
expect(build(:tenant, name: "Name X")).not_to be_valid
end
+ it "is not valid with the name of an already existing hidden record" do
+ insert(:tenant, name: "Name X", hidden_at: Time.current)
+ expect(build(:tenant, name: "Name X")).not_to be_valid
+ end
+
context "Domain schema type" do
before { tenant.schema_type = :domain }
diff --git a/spec/system/admin/tenants_spec.rb b/spec/system/admin/tenants_spec.rb
index 92096261b..38389dbe8 100644
--- a/spec/system/admin/tenants_spec.rb
+++ b/spec/system/admin/tenants_spec.rb
@@ -63,4 +63,43 @@ describe "Tenants", :admin, :seed_tenants do
expect(page).to have_current_path root_path
expect(page).to have_link "Sign in"
end
+
+ scenario "Hide and restore", :show_exceptions do
+ create(:tenant, schema: "moon", name: "Moon")
+
+ visit admin_tenants_path
+
+ within("tr", text: "moon") do
+ expect(page).to have_content "Yes"
+
+ click_button "Enable tenant Moon"
+
+ expect(page).to have_content "No"
+ expect(page).not_to have_link "moon.lvh.me"
+ end
+
+ with_subdomain("moon") do
+ visit root_path
+
+ expect(page).to have_title "Not found"
+ end
+
+ visit admin_tenants_path
+
+ within("tr", text: "moon") do
+ expect(page).to have_content "No"
+
+ click_button "Enable tenant Moon"
+
+ expect(page).to have_content "Yes"
+ expect(page).to have_link "moon.lvh.me"
+ end
+
+ with_subdomain("moon") do
+ visit root_path
+
+ expect(page).to have_link "Sign in"
+ expect(page).not_to have_title "Not found"
+ end
+ end
end