+
<%= t('layout.hey') %> <%= t('layout.back_to_work') %>
diff --git a/app/views/sessions/new.erb b/app/views/sessions/new.erb
index 66f485fd6..cb3d05e2f 100644
--- a/app/views/sessions/new.erb
+++ b/app/views/sessions/new.erb
@@ -19,7 +19,9 @@
<% end %>
-
- <%= t('.sign_up_html', href: setup_password_path) %>
-
+ <% if Setting::UserSignup.enabled? %>
+
+ <%= t('.sign_up_html', href: setup_password_path) %>
+
+ <% end %>
diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb
new file mode 100644
index 000000000..9924c21eb
--- /dev/null
+++ b/app/views/settings/index.html.erb
@@ -0,0 +1,24 @@
+
+ <%= render "feeds/action_bar" %>
+
+
+
+
<%= t('.heading') %>
+
+
<%= t('.description') %>
+
+
+ <% setting = Setting::UserSignup.first %>
+ <%= form_with(model: setting, scope: :setting, url: setting_path(setting)) do |form| %>
+ <% if Setting::UserSignup.enabled? %>
+ <%= form.hidden_field :enabled, value: false %>
+ <%= t('.signup.enabled') %>
+ <%= form.submit(t('.signup.disable'), class: 'btn btn-primary pull-right') %>
+ <% else %>
+ <%= form.hidden_field :enabled, value: true %>
+ <%= t('.signup.disabled') %>
+ <%= form.submit(t('.signup.enable'), class: 'btn btn-primary pull-right') %>
+ <% end %>
+ <% end %>
+
+
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 5db40b9b4..a6e658125 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -66,6 +66,7 @@ en:
subtitle: Let's setup your feeds.
title: Welcome aboard.
layout:
+ admin_settings: Admin Settings
back_to_work: Get back to work, slacker!
export: Export
hey: Hey!
@@ -150,6 +151,15 @@ en:
sign_up_html: Or
sign up.
subtitle: Welcome back, friend.
title: Stringer speaks
+ settings:
+ index:
+ heading: Application Settings
+ description: Edit application wide settings
+ signup:
+ enabled: User signups are enabled
+ disabled: User signups are disabled
+ enable: Enable
+ disable: Disable
starred:
next: Next
of: of
diff --git a/config/routes.rb b/config/routes.rb
index 99abc8616..3016a3e66 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -6,6 +6,7 @@
scope :admin, constraints: AdminConstraint.new do
mount GoodJob::Engine => "good_job"
+ resources :settings, only: [:index, :update]
get "/debug", to: "debug#index"
end
diff --git a/db/migrate/20230721160939_create_settings.rb b/db/migrate/20230721160939_create_settings.rb
new file mode 100644
index 000000000..cd2ec56f3
--- /dev/null
+++ b/db/migrate/20230721160939_create_settings.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class CreateSettings < ActiveRecord::Migration[7.0]
+ def change
+ create_table :settings do |t|
+ t.string :type, null: false, index: { unique: true }
+ t.jsonb :data, null: false, default: {}
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 7159db8d0..bac9e41d5 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[7.0].define(version: 2023_03_30_215830) do
+ActiveRecord::Schema[7.0].define(version: 2023_07_21_160939) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -96,6 +96,14 @@
t.index ["user_id"], name: "index_groups_on_user_id"
end
+ create_table "settings", force: :cascade do |t|
+ t.string "type", null: false
+ t.jsonb "data", default: {}, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["type"], name: "index_settings_on_type", unique: true
+ end
+
create_table "stories", force: :cascade do |t|
t.text "title"
t.text "permalink"
diff --git a/spec/commands/cast_boolean_spec.rb b/spec/commands/cast_boolean_spec.rb
new file mode 100644
index 000000000..6eec442e5
--- /dev/null
+++ b/spec/commands/cast_boolean_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+RSpec.describe CastBoolean do
+ ["true", true, "1"].each do |true_value|
+ it "returns true when passed #{true_value.inspect}" do
+ expect(described_class.call(true_value)).to be(true)
+ end
+ end
+
+ ["false", false, "0"].each do |false_value|
+ it "returns false when passed #{false_value.inspect}" do
+ expect(described_class.call(false_value)).to be(false)
+ end
+ end
+
+ it "raises an error when passed non-boolean value" do
+ ["butt", 0, nil, "", []].each do |bad_value|
+ expected_message = "cannot cast to boolean: #{bad_value.inspect}"
+ expect { described_class.call(bad_value) }
+ .to raise_error(ArgumentError, expected_message)
+ end
+ end
+end
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
new file mode 100644
index 000000000..c02d0c3e9
--- /dev/null
+++ b/spec/models/application_record_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+RSpec.describe ApplicationRecord do
+ describe ".boolean_accessor" do
+ with_model :Cheese, superclass: described_class do
+ table { |t| t.jsonb(:options, default: {}) }
+ end
+
+ describe "#
" do
+ it "returns the value when present" do
+ Cheese.boolean_accessor(:options, :stinky)
+ cheese = Cheese.new(options: { stinky: true })
+
+ expect(cheese.stinky).to be(true)
+ end
+
+ it "returns false by default" do
+ Cheese.boolean_accessor(:options, :stinky)
+ cheese = Cheese.new
+
+ expect(cheese.stinky).to be(false)
+ end
+
+ it "returns the default when value is nil" do
+ Cheese.boolean_accessor(:options, :stinky, default: true)
+ cheese = Cheese.new
+
+ expect(cheese.stinky).to be(true)
+ end
+
+ it "casts the value to a boolean" do
+ Cheese.boolean_accessor(:options, :stinky)
+ cheese = Cheese.new(options: { stinky: "true" })
+
+ expect(cheese.stinky).to be(true)
+ end
+ end
+
+ describe "#=" do
+ it "sets the value" do
+ Cheese.boolean_accessor(:options, :stinky)
+ cheese = Cheese.new
+
+ cheese.stinky = true
+
+ expect(cheese.options).to eq({ "stinky" => true })
+ end
+
+ it "casts the value to a boolean" do
+ Cheese.boolean_accessor(:options, :stinky)
+ cheese = Cheese.new
+
+ cheese.stinky = "true"
+
+ expect(cheese.options).to eq({ "stinky" => true })
+ end
+
+ it "uses the default when value is nil" do
+ Cheese.boolean_accessor(:options, :stinky, default: true)
+ cheese = Cheese.new
+
+ cheese.stinky = nil
+
+ expect(cheese.options).to eq({ "stinky" => true })
+ end
+ end
+ end
+end
diff --git a/spec/models/setting/user_signup_spec.rb b/spec/models/setting/user_signup_spec.rb
new file mode 100644
index 000000000..847443043
--- /dev/null
+++ b/spec/models/setting/user_signup_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+RSpec.describe Setting::UserSignup do
+ describe ".first" do
+ it "returns the first record" do
+ setting = described_class.create!
+
+ expect(described_class.first).to eq(setting)
+ end
+
+ it "creates a record if one does not already exist" do
+ expect { described_class.first }.to change(described_class, :count).by(1)
+ end
+ end
+
+ describe ".enabled?" do
+ it "returns true when enabled" do
+ create(:user)
+ described_class.create!(enabled: true)
+
+ expect(described_class.enabled?).to be(true)
+ end
+
+ it "returns false when disabled" do
+ create(:user)
+ described_class.create!(enabled: false)
+
+ expect(described_class.enabled?).to be(false)
+ end
+
+ it "returns true when no users exist" do
+ described_class.create!(enabled: false)
+
+ expect(described_class.enabled?).to be(true)
+ end
+ end
+end
diff --git a/spec/requests/debug_controller_spec.rb b/spec/requests/debug_controller_spec.rb
index cc49648fe..a8b3cdc96 100644
--- a/spec/requests/debug_controller_spec.rb
+++ b/spec/requests/debug_controller_spec.rb
@@ -10,6 +10,14 @@ def setup
.and_return(["Migration B - 2", "Migration C - 3"])
end
+ it "displays an admin settings link" do
+ setup
+
+ get("/admin/debug")
+
+ expect(rendered).to have_link("Admin Settings", href: settings_path)
+ end
+
it "displays the current Ruby version" do
setup
diff --git a/spec/requests/passwords_controller_spec.rb b/spec/requests/passwords_controller_spec.rb
index 076cc8f04..e0b75a1d2 100644
--- a/spec/requests/passwords_controller_spec.rb
+++ b/spec/requests/passwords_controller_spec.rb
@@ -7,6 +7,15 @@
expect(rendered).to have_selector("form#password_setup")
end
+
+ it "redirects to the login page when signups are not enabled" do
+ create(:user)
+ Setting::UserSignup.update!(enabled: false)
+
+ get "/setup/password"
+
+ expect(response).to redirect_to(login_path)
+ end
end
describe "#create" do
@@ -32,6 +41,15 @@
expect(response).to redirect_to("/feeds/import")
end
+
+ it "redirects to the login page when signups are not enabled" do
+ create(:user)
+ Setting::UserSignup.update!(enabled: false)
+
+ post "/setup/password"
+
+ expect(response).to redirect_to(login_path)
+ end
end
describe "#update" do
diff --git a/spec/requests/settings_controller_spec.rb b/spec/requests/settings_controller_spec.rb
new file mode 100644
index 000000000..a6952f1ee
--- /dev/null
+++ b/spec/requests/settings_controller_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.describe SettingsController do
+ describe "#index" do
+ it "displays the settings page" do
+ login_as(create(:user, admin: true))
+
+ get(settings_path)
+
+ expect(rendered).to have_selector("h1", text: "Settings")
+ .and have_text("User signups are disabled")
+ end
+ end
+
+ describe "#update" do
+ it "allows enabling account creation" do
+ login_as(create(:user, admin: true))
+
+ params = { setting: { enabled: "true" } }
+ put(setting_path(Setting::UserSignup.first), params:)
+
+ expect(Setting::UserSignup.enabled?).to be(true)
+ end
+ end
+end
diff --git a/spec/support/with_model.rb b/spec/support/with_model.rb
new file mode 100644
index 000000000..f7a5f9ba7
--- /dev/null
+++ b/spec/support/with_model.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class WithModel::Model
+ # Workaround for https://github.com/Casecommons/with_model/issues/35
+ def cleanup_descendants_tracking
+ cache_classes = Rails.application.config.cache_classes
+ if defined?(ActiveSupport::DescendantsTracker) && !cache_classes
+ ActiveSupport::DescendantsTracker.clear([@model])
+ elsif @model.superclass.respond_to?(:direct_descendants)
+ @model.superclass.subclasses.delete(@model)
+ end
+ end
+end
+
+RSpec.configure { |config| config.extend(WithModel) }
diff --git a/spec/system/account_setup_spec.rb b/spec/system/account_setup_spec.rb
index 01426b7bc..16ae4eafe 100644
--- a/spec/system/account_setup_spec.rb
+++ b/spec/system/account_setup_spec.rb
@@ -17,13 +17,19 @@ def fill_in_fields(username:)
end
it "allows a second user to sign up" do
+ Setting::UserSignup.create!(enabled: true)
create(:user)
+
visit "/"
- click_link("sign up")
+ expect(page).to have_link("sign up")
+ end
- fill_in_fields(username: "my-username-2")
+ it "does not allow a second user to signup when not enabled" do
+ create(:user)
+
+ visit "/"
- expect(page).to have_text("Logged in as my-username-2")
+ expect(page).not_to have_link("sign up")
end
end
diff --git a/spec/system/application_settings_spec.rb b/spec/system/application_settings_spec.rb
new file mode 100644
index 000000000..03019028c
--- /dev/null
+++ b/spec/system/application_settings_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec.describe "application settings" do
+ it "allows enabling account creation" do
+ login_as(create(:user, admin: true))
+ visit(settings_path)
+
+ within("form", text: "User signups are disabled") { click_on("Enable") }
+
+ expect(page).to have_content("User signups are enabled")
+ end
+end