From bea2c89c2b3b06d67f0a250ed46666405a527214 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Nov 2025 17:15:26 +0100 Subject: [PATCH] Add new has_json to add Account#settings to restrict room creation to only administrators --- app/controllers/accounts_controller.rb | 2 +- app/helpers/accounts_helper.rb | 9 ++++ app/models/account.rb | 1 + app/views/accounts/edit.html.erb | 6 +++ .../20251126130131_add_account_settings.rb | 5 ++ lib/rails_ext/active_record_has_json.rb | 52 +++++++++++++++++++ test/models/account_test.rb | 14 +++++ 7 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20251126130131_add_account_settings.rb create mode 100644 lib/rails_ext/active_record_has_json.rb diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index be05d66..462ae01 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -17,7 +17,7 @@ class AccountsController < ApplicationController end def account_params - params.require(:account).permit(:name, :logo) + params.require(:account).permit(:name, :logo, settings: {}) end def account_users diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index 559e195..22c6c4a 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -2,4 +2,13 @@ module AccountsHelper def account_logo_tag(style: nil) tag.figure image_tag(fresh_account_logo_path, alt: "Account logo", size: 300), class: "account-logo avatar #{style}" end + + def button_to_toggle_setting(label, setting) + button_to account_path(account: { settings: { setting => !Current.account.settings.send("#{setting}?") } }), + method: :put, + role: "checkbox", aria: { checked: true, labelledby: "#{setting}_account_setting" }, tabindex: 0, + class: "btn" do + tag.span(label, id: "#{setting}_account_setting") + end + end end diff --git a/app/models/account.rb b/app/models/account.rb index 36a7648..71ceb7c 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -2,4 +2,5 @@ class Account < ApplicationRecord include Joinable has_one_attached :logo + has_json settings: { restrict_room_creation_to_administrators: :boolean }, delegate: true end diff --git a/app/views/accounts/edit.html.erb b/app/views/accounts/edit.html.erb index 93a828a..2b6d5ab 100644 --- a/app/views/accounts/edit.html.erb +++ b/app/views/accounts/edit.html.erb @@ -64,6 +64,12 @@ <% end %> <% end %> + + <% if Current.account.restrict_room_creation_to_administrators? %> + <%= button_to_toggle_setting "Allow everyone to create new rooms", :restrict_room_creation_to_administrators %> + <% else %> + <%= button_to_toggle_setting "Only allow admins to create new rooms", :restrict_room_creation_to_administrators %> + <% end %> <% else %> <%= account_logo_tag style: "txt-xx-large center" %>

<%= @account.name %>

diff --git a/db/migrate/20251126130131_add_account_settings.rb b/db/migrate/20251126130131_add_account_settings.rb new file mode 100644 index 0000000..6c97f4e --- /dev/null +++ b/db/migrate/20251126130131_add_account_settings.rb @@ -0,0 +1,5 @@ +class AddAccountSettings < ActiveRecord::Migration[7.2] + def change + add_column :accounts, :settings, :json, default: {} + end +end diff --git a/lib/rails_ext/active_record_has_json.rb b/lib/rails_ext/active_record_has_json.rb new file mode 100644 index 0000000..8a3bdd7 --- /dev/null +++ b/lib/rails_ext/active_record_has_json.rb @@ -0,0 +1,52 @@ +module ActiveRecord + class TypedJson + def initialize(schema, data:) + @schema, @data = schema, data + end + + def assign_data_with_type_casting(new_data) + new_data.each do |key, value| + @data[key] = ActiveModel::Type.lookup(@schema[key.to_sym]).cast(value) + end + end + + private + def method_missing(method_name, *args, **kwargs) + key = method_name.to_s.remove(/(\?|=)/) + + if key_type = @schema[key.to_sym] + if method_name.ends_with?("?") + @data[key].present? + elsif method_name.ends_with?("=") + value = args.first + @data[key] = ActiveModel::Type.lookup(key_type).cast(value) + else + @data.fetch(key) + end + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @schema.key?(method_name.to_s.remove(/[?=]/).to_sym) || super + end + end + + class Base + class << self + def has_json(delegate: false, **schemas) + schemas.each do |name, schema| + define_method(name) { ActiveRecord::TypedJson.new(schema, data: self[name]) } + define_method("#{name}=") { |data| send(name).assign_data_with_type_casting(data) } + + schema.keys.each do |schema_key| + define_method(schema_key) { send(name).send(schema_key) } + define_method("#{schema_key}?") { send(name).send("#{schema_key}?") } + define_method("#{schema_key}=") { |value| send(name).send("#{schema_key}=", value) } + end if delegate + end + end + end + end +end diff --git a/test/models/account_test.rb b/test/models/account_test.rb index 94d4f83..e4e3673 100644 --- a/test/models/account_test.rb +++ b/test/models/account_test.rb @@ -1,4 +1,18 @@ require "test_helper" class AccountTest < ActiveSupport::TestCase + test "settings" do + accounts(:signal).restrict_room_creation_to_administrators = true + assert accounts(:signal).restrict_room_creation_to_administrators? + assert_equal({ "restrict_room_creation_to_administrators" => true }, accounts(:signal)[:settings]) + + accounts(:signal).update!(settings: { "restrict_room_creation_to_administrators" => "true" }) + assert accounts(:signal).reload.restrict_room_creation_to_administrators? + + accounts(:signal).restrict_room_creation_to_administrators = false + assert_not accounts(:signal).restrict_room_creation_to_administrators? + assert_equal({ "restrict_room_creation_to_administrators" => false }, accounts(:signal)[:settings]) + accounts(:signal).update!(settings: { "restrict_room_creation_to_administrators" => "false" }) + assert_not accounts(:signal).reload.restrict_room_creation_to_administrators? + end end