From e9059107191f06ef15645da14c63691bce4eaa59 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Tue, 4 Jun 2024 00:45:33 +0200 Subject: [PATCH 01/53] Import previously missed settings to *.dist --- config/application.rb.dist | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/application.rb.dist b/config/application.rb.dist index adce1d6..c025725 100644 --- a/config/application.rb.dist +++ b/config/application.rb.dist @@ -23,6 +23,10 @@ module FixinMe # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.0 + # Autoload lib/, required e.g. for core library extensions. + # https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#config-autoload-lib-ignore. + config.autoload_lib(ignore: %w(assets tasks)) + # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files @@ -32,7 +36,7 @@ module FixinMe # config.eager_load_paths << Rails.root.join("extras") config.action_dispatch.rescue_responses['ApplicationController::AccessForbidden'] = :forbidden - config.action_dispatch.rescue_responses['ArgumentError'] = :bad_request + config.action_dispatch.rescue_responses['ApplicationController::ParameterInvalid'] = :unprocessable_entity # SETUP: Below settings need to be updated on a per-installation basis. # -- 2.49.1 From 769e4af6031eef7d63e172b911fc91f71d2b4215 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Tue, 4 Jun 2024 00:47:50 +0200 Subject: [PATCH 02/53] Bring per-installation setting to application.rb --- config/application.rb.dist | 3 +++ config/initializers/devise.rb | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/config/application.rb.dist b/config/application.rb.dist index c025725..d28d914 100644 --- a/config/application.rb.dist +++ b/config/application.rb.dist @@ -51,5 +51,8 @@ module FixinMe # Email address of admin account config.admin = 'admin@localhost' + + # Sender address of account registration-related messages + Devise.mailer_sender = 'noreply@localhost' end end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index c7f707c..5012fdc 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -24,7 +24,8 @@ Devise.setup do |config| # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. - config.mailer_sender = 'fixinme@noreply.me' + # This is set in 'config/application.rb'. + #config.mailer_sender = 'fixinme@noreply.me' # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' -- 2.49.1 From c6a7838df1a64b5e841f933e90d024dcb9b958e9 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 9 Nov 2024 02:02:01 +0100 Subject: [PATCH 03/53] Change 'Back' button to tab --- app/views/users/registrations/edit.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/users/registrations/edit.html.erb b/app/views/users/registrations/edit.html.erb index 0a6aeef..28c323e 100644 --- a/app/views/users/registrations/edit.html.erb +++ b/app/views/users/registrations/edit.html.erb @@ -1,6 +1,6 @@ <% content_for :navigation, flush: true do %> - <%= image_link_to t(:back), 'arrow-left-bold-outline', - request.referer.present? ? :back : root_path %> + <%= link_to svg_tag("pictograms/arrow-left-bold-outline") + t(:back), + request.referer.present? ? :back : root_path, class: 'tab' %> <% end %>
-- 2.49.1 From be48d6fd7f398a82558fa6e6d95f2508fd5cd1a2 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 9 Nov 2024 02:03:34 +0100 Subject: [PATCH 04/53] Use Arel::FactoryMehods for coalesce() --- app/models/unit.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index 6324fcb..4e88fa2 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -15,12 +15,9 @@ class Unit < ApplicationRecord scope :defaults, ->{ where(user: nil) } scope :ordered, ->{ - parent_symbol = Arel::Nodes::NamedFunction.new( - 'COALESCE', - [Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]] - ) left_outer_joins(:base) - .order(parent_symbol, arel_table[:base_id].asc.nulls_first, :multiplier, :symbol) + .order(arel_table.coalesce(Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]), + arel_table[:base_id].asc.nulls_first, :multiplier, :symbol) } before_destroy do -- 2.49.1 From 846eb6da14fe794332c548b7c03d9290466221a1 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 9 Nov 2024 02:05:04 +0100 Subject: [PATCH 05/53] Preliminary support for default Units import --- app/models/unit.rb | 18 ++++++++++++++++++ app/views/units/index.html.erb | 2 +- config/routes.rb | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index 4e88fa2..28b38ba 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -14,6 +14,24 @@ class Unit < ApplicationRecord validates :multiplier, numericality: {other_than: 0}, if: :base scope :defaults, ->{ where(user: nil) } + scope :with_defaults, ->{ self.or(Unit.where(user: nil)) } + scope :default_diff, ->{ + other_units = Unit.arel_table.alias('other_units') + other_bases_units = Unit.arel_table.alias('other_bases_units') + constraints = other_bases_units[:id].eq(other_units[:base_id]) + .and(other_units[:symbol].eq(arel_table[:symbol])) + .and(other_units[:user_id].not_eq(arel_table[:user_id])) + + with_defaults + .joins( + arel_table.create_join( + arel_table.grouping([other_units, other_bases_units]), + arel_table.create_on(constraints), + Arel::Nodes::OuterJoin + ).to_sql + ) + .where({other_units: {id: nil}}) + } scope :ordered, ->{ left_outer_joins(:base) .order(arel_table.coalesce(Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]), diff --git a/app/views/units/index.html.erb b/app/views/units/index.html.erb index 19177c8..778b688 100644 --- a/app/views/units/index.html.erb +++ b/app/views/units/index.html.erb @@ -2,7 +2,7 @@ <% if current_user.at_least(:active) %> <%= image_link_to t('.add_unit'), 'plus-outline', new_unit_path, id: :add_unit, onclick: 'this.blur();', data: {turbo_stream: true} %> - <%= image_link_to t('.import_units'), 'import', new_unit_path, class: 'tools', + <%= image_link_to t('.import_units'), 'import', units_defaults_path, class: 'tools', data: {turbo_stream: true} %> <% end %>
diff --git a/config/routes.rb b/config/routes.rb index 3f556c2..8695bcd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,7 +9,7 @@ Rails.application.routes.draw do end namespace :units do - get 'defaults/index' + resources :defaults, only: :index end resources :users, only: [:index, :show, :update] do -- 2.49.1 From f899fed91067664223ed31cabd4b2cc83ce9d8c5 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 9 Nov 2024 15:56:22 +0100 Subject: [PATCH 06/53] Prefer icons without circle --- app/assets/images/pictograms/close-outline.svg | 1 + app/views/units/_form.html.erb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 app/assets/images/pictograms/close-outline.svg diff --git a/app/assets/images/pictograms/close-outline.svg b/app/assets/images/pictograms/close-outline.svg new file mode 100644 index 0000000..46c536e --- /dev/null +++ b/app/assets/images/pictograms/close-outline.svg @@ -0,0 +1 @@ + diff --git a/app/views/units/_form.html.erb b/app/views/units/_form.html.erb index 249612c..e2dd699 100644 --- a/app/views/units/_form.html.erb +++ b/app/views/units/_form.html.erb @@ -20,8 +20,9 @@ <%= form.submit form: :unit_form %> - <%= image_link_to t(:cancel), "close-circle-outline", units_path, class: 'dangerous', + <%= image_link_to t(:cancel), "close-outline", units_path, class: 'dangerous', name: :cancel, onclick: render_turbo_stream('form_close', {link_id: link_id}) %> + <% end %> <% end %> -- 2.49.1 From f402e6ba00f662e246ed7a50137fc7a3ffcec064 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 9 Nov 2024 15:58:01 +0100 Subject: [PATCH 07/53] Add source of icons --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2de7d16..c5c1087 100644 --- a/README.md +++ b/README.md @@ -118,3 +118,6 @@ Tests need to be run from within toplevel application directory: bundle exec rails test test/system/users_test.rb --seed 1234 +### Icons + +Pictogrammers Material Design Icons: https://pictogrammers.com/library/mdi/ -- 2.49.1 From 537cd18336400514608a0272fee73fb55f283bb0 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 9 Nov 2024 21:12:51 +0100 Subject: [PATCH 08/53] Change namespace for defaults controllers To allow proper path prefix for view partials when using config.action_view.prefix_partial_path_with_controller_namespace --- .../images/pictograms/download-outline.svg | 1 + app/assets/images/pictograms/upload-outline.svg | 1 + .../units_controller.rb} | 3 ++- app/helpers/default/units_helper.rb | 2 ++ app/helpers/units/defaults_helper.rb | 2 -- app/views/default/units/index.html.erb | 17 +++++++++++++++++ app/views/units/defaults/index.html.erb | 2 -- app/views/units/index.html.erb | 3 +-- config/locales/en.yml | 4 ++++ config/routes.rb | 4 ++-- .../units_controller_test.rb} | 2 +- 11 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 app/assets/images/pictograms/download-outline.svg create mode 100644 app/assets/images/pictograms/upload-outline.svg rename app/controllers/{units/defaults_controller.rb => default/units_controller.rb} (59%) create mode 100644 app/helpers/default/units_helper.rb delete mode 100644 app/helpers/units/defaults_helper.rb create mode 100644 app/views/default/units/index.html.erb delete mode 100644 app/views/units/defaults/index.html.erb rename test/controllers/{units/defaults_controller_test.rb => default/units_controller_test.rb} (63%) diff --git a/app/assets/images/pictograms/download-outline.svg b/app/assets/images/pictograms/download-outline.svg new file mode 100644 index 0000000..71466c0 --- /dev/null +++ b/app/assets/images/pictograms/download-outline.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/pictograms/upload-outline.svg b/app/assets/images/pictograms/upload-outline.svg new file mode 100644 index 0000000..de60d1e --- /dev/null +++ b/app/assets/images/pictograms/upload-outline.svg @@ -0,0 +1 @@ + diff --git a/app/controllers/units/defaults_controller.rb b/app/controllers/default/units_controller.rb similarity index 59% rename from app/controllers/units/defaults_controller.rb rename to app/controllers/default/units_controller.rb index 9a635d3..112ccd8 100644 --- a/app/controllers/units/defaults_controller.rb +++ b/app/controllers/default/units_controller.rb @@ -1,4 +1,4 @@ -class Units::DefaultsController < ApplicationController +class Default::UnitsController < ApplicationController navigation_tab :units before_action except: :index do @@ -6,5 +6,6 @@ class Units::DefaultsController < ApplicationController end def index + @units = current_user.units.defaults_diff end end diff --git a/app/helpers/default/units_helper.rb b/app/helpers/default/units_helper.rb new file mode 100644 index 0000000..ca7f755 --- /dev/null +++ b/app/helpers/default/units_helper.rb @@ -0,0 +1,2 @@ +module Default::UnitsHelper +end diff --git a/app/helpers/units/defaults_helper.rb b/app/helpers/units/defaults_helper.rb deleted file mode 100644 index 9721ef5..0000000 --- a/app/helpers/units/defaults_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module Units::DefaultsHelper -end diff --git a/app/views/default/units/index.html.erb b/app/views/default/units/index.html.erb new file mode 100644 index 0000000..321f6a0 --- /dev/null +++ b/app/views/default/units/index.html.erb @@ -0,0 +1,17 @@ +
+ <%= image_link_to t('.back'), 'arrow-left-bold-outline', units_path %> +
+ + + + + + <% if current_user.at_least(:active) %> + + <% end %> + + + + <%= render(@units, partial: 'default') || render_no_items %> + +
<%= User.human_attribute_name(:symbol).capitalize %><%= t :actions %>
diff --git a/app/views/units/defaults/index.html.erb b/app/views/units/defaults/index.html.erb deleted file mode 100644 index 34466c7..0000000 --- a/app/views/units/defaults/index.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -

Units::Defaults#index

-

Find me in app/views/units/defaults/index.html.erb

diff --git a/app/views/units/index.html.erb b/app/views/units/index.html.erb index 778b688..c6d4aa2 100644 --- a/app/views/units/index.html.erb +++ b/app/views/units/index.html.erb @@ -2,9 +2,8 @@ <% if current_user.at_least(:active) %> <%= image_link_to t('.add_unit'), 'plus-outline', new_unit_path, id: :add_unit, onclick: 'this.blur();', data: {turbo_stream: true} %> - <%= image_link_to t('.import_units'), 'import', units_defaults_path, class: 'tools', - data: {turbo_stream: true} %> <% end %> + <%= image_link_to t('.import_units'), 'download-outline', default_units_path, class: 'tools' %> <%= tag.div id: :unit_form %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 06f86ef..9f7e00c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -67,6 +67,10 @@ en: multiplier_reset: Multiplier of "%{symbol}" has been reset to 1, due to repositioning destroy: success: Deleted unit + default: + units: + index: + back: Back to units... users: index: disguise: View as... diff --git a/config/routes.rb b/config/routes.rb index 8695bcd..3aaf890 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,8 +8,8 @@ Rails.application.routes.draw do end end - namespace :units do - resources :defaults, only: :index + namespace :default do + resources :units, only: :index end resources :users, only: [:index, :show, :update] do diff --git a/test/controllers/units/defaults_controller_test.rb b/test/controllers/default/units_controller_test.rb similarity index 63% rename from test/controllers/units/defaults_controller_test.rb rename to test/controllers/default/units_controller_test.rb index b01dfe2..2742778 100644 --- a/test/controllers/units/defaults_controller_test.rb +++ b/test/controllers/default/units_controller_test.rb @@ -1,6 +1,6 @@ require "test_helper" -class Units::DefaultsControllerTest < ActionDispatch::IntegrationTest +class Default::UnitsControllerTest < ActionDispatch::IntegrationTest test "should get index" do get units_defaults_index_url assert_response :success -- 2.49.1 From 817b1a43760170094a7ae21fd9df613c75c24ab2 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 10 Nov 2024 17:34:02 +0100 Subject: [PATCH 09/53] Update permission checking --- app/controllers/default/units_controller.rb | 21 ++++++++++++++++++++- app/controllers/units_controller.rb | 2 +- app/controllers/users_controller.rb | 13 ++++++++----- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/app/controllers/default/units_controller.rb b/app/controllers/default/units_controller.rb index 112ccd8..e2255e9 100644 --- a/app/controllers/default/units_controller.rb +++ b/app/controllers/default/units_controller.rb @@ -1,11 +1,30 @@ class Default::UnitsController < ApplicationController navigation_tab :units + before_action :find_unit, only: [:import, :export, :destroy] + before_action except: :index do - raise AccessForbidden unless current_user.at_least(:admin) + case action_name.to_sym + when :import, :import_all + raise AccessForbidden unless current_user.at_least(:active) + else + raise AccessForbidden unless current_user.at_least(:admin) + end end def index @units = current_user.units.defaults_diff end + + def import + end + + def import_all + end + + def export + end + + def destroy + end end diff --git a/app/controllers/units_controller.rb b/app/controllers/units_controller.rb index 800bfe8..2231f04 100644 --- a/app/controllers/units_controller.rb +++ b/app/controllers/units_controller.rb @@ -1,5 +1,5 @@ class UnitsController < ApplicationController - before_action only: [:new] do + before_action only: :new do find_unit if params[:id].present? end before_action :find_unit, only: [:edit, :update, :rebase, :destroy] diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 29f4cbb..0472ccf 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -2,11 +2,14 @@ class UsersController < ApplicationController helper_method :allow_disguise? before_action :find_user, only: [:show, :update, :disguise] - before_action except: :revert do - raise AccessForbidden unless current_user.at_least(:admin) - end - before_action only: :revert do - raise AccessForbidden unless current_user_disguised? + + before_action do + case action_name.to_sym + when :revert + raise AccessForbidden unless current_user_disguised? + else + raise AccessForbidden unless current_user.at_least(:admin) + end end def index -- 2.49.1 From aebbe11bef44863346d61eab1ad78f731d019729 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 10 Nov 2024 17:34:43 +0100 Subject: [PATCH 10/53] Add default Units actions --- config/routes.rb | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 3aaf890..06c1241 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,22 +3,19 @@ Rails.application.routes.draw do controllers: {registrations: :registrations} resources :units, except: [:show], path_names: {new: '(/:id)/new'} do - member do - post :rebase - end + member { post :rebase } end namespace :default do - resources :units, only: :index + resources :units, only: [:index, :destroy] do + member { post :import, :export } + collection { post :import_all } + end end resources :users, only: [:index, :show, :update] do - member do - get :disguise - end - collection do - get :revert - end + member { get :disguise } + collection { get :revert } end devise_scope :user do -- 2.49.1 From 51011951f9943d2e2325a80d0eca0d70fc9d77da Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 10 Nov 2024 21:30:19 +0100 Subject: [PATCH 11/53] Default Units index --- .../pictograms/download-multiple-outline.svg | 1 + app/models/unit.rb | 7 ++++++- app/views/default/units/_unit.html.erb | 19 +++++++++++++++++++ app/views/default/units/index.html.erb | 8 ++++++-- config/locales/en.yml | 3 +++ 5 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 app/assets/images/pictograms/download-multiple-outline.svg create mode 100644 app/views/default/units/_unit.html.erb diff --git a/app/assets/images/pictograms/download-multiple-outline.svg b/app/assets/images/pictograms/download-multiple-outline.svg new file mode 100644 index 0000000..24d0398 --- /dev/null +++ b/app/assets/images/pictograms/download-multiple-outline.svg @@ -0,0 +1 @@ + diff --git a/app/models/unit.rb b/app/models/unit.rb index 28b38ba..471bc5d 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -15,10 +15,11 @@ class Unit < ApplicationRecord scope :defaults, ->{ where(user: nil) } scope :with_defaults, ->{ self.or(Unit.where(user: nil)) } - scope :default_diff, ->{ + scope :defaults_diff, ->{ other_units = Unit.arel_table.alias('other_units') other_bases_units = Unit.arel_table.alias('other_bases_units') constraints = other_bases_units[:id].eq(other_units[:base_id]) + .and(other_bases_units[:symbol].eq(Arel::Table.new(:bases_units)[:symbol])) .and(other_units[:symbol].eq(arel_table[:symbol])) .and(other_units[:user_id].not_eq(arel_table[:user_id])) @@ -46,4 +47,8 @@ class Unit < ApplicationRecord def movable? subunits.empty? end + + def default? + user.nil? + end end diff --git a/app/views/default/units/_unit.html.erb b/app/views/default/units/_unit.html.erb new file mode 100644 index 0000000..662a7ea --- /dev/null +++ b/app/views/default/units/_unit.html.erb @@ -0,0 +1,19 @@ +<%= tag.tr do %> + + <%= unit.symbol %> + + + + <% if current_user.at_least(:active) && unit.default? %> + <%= image_button_to t(".import"), "download-outline", import_default_unit_path(unit) %> + <% end %> + <% if current_user.at_least(:admin) %> + <% if !unit.default? %> + <%= image_button_to t(".export"), "upload-outline", export_default_unit_path(unit) %> + <% else %> + <%= image_button_to t(".delete"), "delete-outline", unit_path(unit), + method: :delete %> + <% end %> + <% end %> + +<% end %> diff --git a/app/views/default/units/index.html.erb b/app/views/default/units/index.html.erb index 321f6a0..52e6466 100644 --- a/app/views/default/units/index.html.erb +++ b/app/views/default/units/index.html.erb @@ -1,5 +1,9 @@
- <%= image_link_to t('.back'), 'arrow-left-bold-outline', units_path %> + <% if current_user.at_least(:active) %> + <%= image_link_to t('.import_all'), 'download-multiple-outline', + import_all_default_units_path, data: {turbo_stream: true} %> + <% end %> + <%= image_link_to t('.back'), 'arrow-left-bold-outline', units_path, class: 'tools' %>
@@ -12,6 +16,6 @@ - <%= render(@units, partial: 'default') || render_no_items %> + <%= render(@units) || render_no_items %>
diff --git a/config/locales/en.yml b/config/locales/en.yml index 9f7e00c..6370bba 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -69,7 +69,10 @@ en: success: Deleted unit default: units: + unit: + delete_default: Delete default index: + import_all: Import all back: Back to units... users: index: -- 2.49.1 From 9a02f0b0ae8d47efcd3c52aa023fa0504c816493 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 10 Nov 2024 21:46:00 +0100 Subject: [PATCH 12/53] Fix quotes and translations --- app/views/default/units/_unit.html.erb | 6 +++--- app/views/units/_unit.html.erb | 4 ++-- config/locales/en.yml | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/views/default/units/_unit.html.erb b/app/views/default/units/_unit.html.erb index 662a7ea..7785b6b 100644 --- a/app/views/default/units/_unit.html.erb +++ b/app/views/default/units/_unit.html.erb @@ -5,13 +5,13 @@ <% if current_user.at_least(:active) && unit.default? %> - <%= image_button_to t(".import"), "download-outline", import_default_unit_path(unit) %> + <%= image_button_to t('.import'), 'download-outline', import_default_unit_path(unit) %> <% end %> <% if current_user.at_least(:admin) %> <% if !unit.default? %> - <%= image_button_to t(".export"), "upload-outline", export_default_unit_path(unit) %> + <%= image_button_to t('.export'), 'upload-outline', export_default_unit_path(unit) %> <% else %> - <%= image_button_to t(".delete"), "delete-outline", unit_path(unit), + <%= image_button_to t('.delete'), 'delete-outline', unit_path(unit), method: :delete %> <% end %> <% end %> diff --git a/app/views/units/_unit.html.erb b/app/views/units/_unit.html.erb index f970250..7039a05 100644 --- a/app/views/units/_unit.html.erb +++ b/app/views/units/_unit.html.erb @@ -14,12 +14,12 @@ <% if current_user.at_least(:active) %> <% if unit.base.nil? %> - <%= image_link_to t(".add_subunit"), "plus-outline", new_unit_path(unit), + <%= image_link_to t('.add_subunit'), 'plus-outline', new_unit_path(unit), id: dom_id(unit, :add), onclick: 'this.blur();', data: {turbo_stream: true} %> <% end %> - <%= image_button_to t(".delete_unit"), "delete-outline", unit_path(unit), + <%= image_button_to t('.delete_unit'), 'delete-outline', unit_path(unit), method: :delete %> <% if unit.movable? %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 6370bba..f36dccd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -70,7 +70,9 @@ en: default: units: unit: - delete_default: Delete default + delete: Delete + export: Export + import: Import index: import_all: Import all back: Back to units... -- 2.49.1 From 4c09989788c57849700e370911d8c155d0f42499 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Mon, 11 Nov 2024 15:43:20 +0100 Subject: [PATCH 13/53] Add translation --- app/views/default/units/index.html.erb | 2 +- config/locales/en.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/default/units/index.html.erb b/app/views/default/units/index.html.erb index 52e6466..e25331d 100644 --- a/app/views/default/units/index.html.erb +++ b/app/views/default/units/index.html.erb @@ -11,7 +11,7 @@ <%= User.human_attribute_name(:symbol).capitalize %> <% if current_user.at_least(:active) %> - <%= t :actions %> + <%= t '.actions' %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index f36dccd..2e45765 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -74,6 +74,7 @@ en: export: Export import: Import index: + actions: Actions on defaults import_all: Import all back: Back to units... users: -- 2.49.1 From 7234d60afc01c3eaeefc0ee695633250c5bd6880 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Thu, 14 Nov 2024 02:50:53 +0100 Subject: [PATCH 14/53] Replace additional joins with NOT EXISTS subquery --- app/models/unit.rb | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index 471bc5d..96beb7e 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -18,20 +18,22 @@ class Unit < ApplicationRecord scope :defaults_diff, ->{ other_units = Unit.arel_table.alias('other_units') other_bases_units = Unit.arel_table.alias('other_bases_units') - constraints = other_bases_units[:id].eq(other_units[:base_id]) - .and(other_bases_units[:symbol].eq(Arel::Table.new(:bases_units)[:symbol])) - .and(other_units[:symbol].eq(arel_table[:symbol])) - .and(other_units[:user_id].not_eq(arel_table[:user_id])) + # add 'portable' fields (import on !default == export) to select with_defaults - .joins( - arel_table.create_join( - arel_table.grouping([other_units, other_bases_units]), - arel_table.create_on(constraints), - Arel::Nodes::OuterJoin - ).to_sql - ) - .where({other_units: {id: nil}}) + .where("NOT EXISTS (?)", + Unit.select(1).from(other_units).joins( + arel_table.create_join( + other_bases_units, + arel_table.create_on(other_bases_units[:id].eq(other_units[:base_id])), + Arel::Nodes::OuterJoin + ) + ).where( + other_bases_units[:symbol].eq(Arel::Table.new(:bases_units)[:symbol]) + .and(other_units[:symbol].eq(arel_table[:symbol])) + .and(other_units[:user_id].not_eq(arel_table[:user_id])) + ) + ) } scope :ordered, ->{ left_outer_joins(:base) -- 2.49.1 From a01c89ce3accdf2bc8a1c60897426ead945adc24 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Thu, 14 Nov 2024 04:28:20 +0100 Subject: [PATCH 15/53] Further simplify EXISTS condition with SelectManager --- app/models/unit.rb | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index 96beb7e..5abe06d 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -21,19 +21,16 @@ class Unit < ApplicationRecord # add 'portable' fields (import on !default == export) to select with_defaults - .where("NOT EXISTS (?)", - Unit.select(1).from(other_units).joins( - arel_table.create_join( - other_bases_units, - arel_table.create_on(other_bases_units[:id].eq(other_units[:base_id])), - Arel::Nodes::OuterJoin - ) - ).where( - other_bases_units[:symbol].eq(Arel::Table.new(:bases_units)[:symbol]) - .and(other_units[:symbol].eq(arel_table[:symbol])) - .and(other_units[:user_id].not_eq(arel_table[:user_id])) - ) - ) + .where.not( + Arel::SelectManager.new.from(other_units) + .outer_join(other_bases_units) + .on(other_units[:base_id].eq(other_bases_units[:id])) + .where( + other_bases_units[:symbol].eq(Arel::Table.new(:bases_units)[:symbol]) + .and(other_units[:symbol].eq(arel_table[:symbol])) + .and(other_units[:user_id].not_eq(arel_table[:user_id])) + ).project(1).exists + ) } scope :ordered, ->{ left_outer_joins(:base) -- 2.49.1 From 4447735dce4e4de44a1bbd43ab8aa0c6c7db3eb1 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Fri, 15 Nov 2024 02:02:19 +0100 Subject: [PATCH 16/53] First part of portability checks --- app/controllers/default/units_controller.rb | 2 +- app/controllers/units_controller.rb | 2 +- app/models/unit.rb | 31 ++++++++++++++++----- app/models/user.rb | 2 +- app/views/default/units/_unit.html.erb | 3 +- 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/app/controllers/default/units_controller.rb b/app/controllers/default/units_controller.rb index e2255e9..ea47bbb 100644 --- a/app/controllers/default/units_controller.rb +++ b/app/controllers/default/units_controller.rb @@ -13,7 +13,7 @@ class Default::UnitsController < ApplicationController end def index - @units = current_user.units.defaults_diff + @units = current_user.units.defaults_diff.includes(:base).ordered end def import diff --git a/app/controllers/units_controller.rb b/app/controllers/units_controller.rb index 2231f04..f48cfef 100644 --- a/app/controllers/units_controller.rb +++ b/app/controllers/units_controller.rb @@ -9,7 +9,7 @@ class UnitsController < ApplicationController end def index - @units = current_user.units.includes(:subunits) + @units = current_user.units.includes(:subunits).ordered end def new diff --git a/app/models/unit.rb b/app/models/unit.rb index 5abe06d..2a5fa26 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -16,21 +16,34 @@ class Unit < ApplicationRecord scope :defaults, ->{ where(user: nil) } scope :with_defaults, ->{ self.or(Unit.where(user: nil)) } scope :defaults_diff, ->{ - other_units = Unit.arel_table.alias('other_units') - other_bases_units = Unit.arel_table.alias('other_bases_units') + bases_units = arel_table.alias('bases_units') + other_units = arel_table.alias('other_units') + other_bases_units = arel_table.alias('other_bases_units') + parent_units = arel_table.alias('parent_units') - # add 'portable' fields (import on !default == export) to select - with_defaults + with(units: self.with_defaults).unscope(where: :user_id).left_joins(:base) .where.not( - Arel::SelectManager.new.from(other_units) + Arel::SelectManager.new.project(1).from(other_units) .outer_join(other_bases_units) .on(other_units[:base_id].eq(other_bases_units[:id])) .where( - other_bases_units[:symbol].eq(Arel::Table.new(:bases_units)[:symbol]) + other_bases_units[:symbol].eq(bases_units[:symbol]) .and(other_units[:symbol].eq(arel_table[:symbol])) .and(other_units[:user_id].not_eq(arel_table[:user_id])) - ).project(1).exists + ).exists + ).joins( + arel_table.create_join(parent_units, + arel_table.create_on( + parent_units[:symbol].eq(bases_units[:symbol]) + .and(parent_units[:user_id].not_eq(bases_units[:user_id])) + ), + Arel::Nodes::OuterJoin) + ).select( + arel_table[Arel.star], + Arel::Nodes::IsNotDistinctFrom.new(parent_units[:symbol], bases_units[:symbol]) + .as('portable') ) + # complete portability check with children } scope :ordered, ->{ left_outer_joins(:base) @@ -50,4 +63,8 @@ class Unit < ApplicationRecord def default? user.nil? end + + def exportable? + !default? && (base.nil? || base.default?) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 900d6ea..3c43d9b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,7 +11,7 @@ class User < ApplicationRecord disabled: 0, # administratively disallowed to sign in }, default: :active - has_many :units, -> { ordered }, dependent: :destroy + has_many :units, dependent: :destroy def at_least(status) User.statuses[self.status] >= User.statuses[status] diff --git a/app/views/default/units/_unit.html.erb b/app/views/default/units/_unit.html.erb index 7785b6b..53d8889 100644 --- a/app/views/default/units/_unit.html.erb +++ b/app/views/default/units/_unit.html.erb @@ -5,7 +5,8 @@ <% if current_user.at_least(:active) && unit.default? %> - <%= image_button_to t('.import'), 'download-outline', import_default_unit_path(unit) %> + <%= image_button_to t('.import'), 'download-outline', import_default_unit_path(unit), + disabled: !unit.portable? %> <% end %> <% if current_user.at_least(:admin) %> <% if !unit.default? %> -- 2.49.1 From d9e74ed3059f8010a4098f7147353b99d047e071 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Fri, 15 Nov 2024 19:45:55 +0100 Subject: [PATCH 17/53] Avoid unscoping --- app/models/unit.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index 2a5fa26..e9025f8 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -21,7 +21,8 @@ class Unit < ApplicationRecord other_bases_units = arel_table.alias('other_bases_units') parent_units = arel_table.alias('parent_units') - with(units: self.with_defaults).unscope(where: :user_id).left_joins(:base) + Unit.with(units: self.with_defaults).left_joins(:base) + # Exclude Units that are/have default counterpart .where.not( Arel::SelectManager.new.project(1).from(other_units) .outer_join(other_bases_units) @@ -31,6 +32,7 @@ class Unit < ApplicationRecord .and(other_units[:symbol].eq(arel_table[:symbol])) .and(other_units[:user_id].not_eq(arel_table[:user_id])) ).exists + # Decide if Unit can be im-/exported based on existing hierarchy ).joins( arel_table.create_join(parent_units, arel_table.create_on( @@ -61,7 +63,7 @@ class Unit < ApplicationRecord end def default? - user.nil? + user_id.nil? end def exportable? -- 2.49.1 From 41982e9dbc2f5e7513dd38fb07cdd9a811ec8dea Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 16 Nov 2024 02:31:53 +0100 Subject: [PATCH 18/53] Import portability checks complete --- app/assets/stylesheets/application.css | 3 ++- app/models/unit.rb | 34 +++++++++++++++++--------- app/views/default/units/_unit.html.erb | 7 +++--- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 4c49e38..14ec9a7 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -410,7 +410,8 @@ table.items td.link a:hover:focus-visible { color: #006c9b; } -table.items td:not(:first-child) { +table.items td:not(:first-child), +.grayed { color: var(--color-table-gray); fill: var(--color-table-gray); } diff --git a/app/models/unit.rb b/app/models/unit.rb index e9025f8..53af7df 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -19,7 +19,7 @@ class Unit < ApplicationRecord bases_units = arel_table.alias('bases_units') other_units = arel_table.alias('other_units') other_bases_units = arel_table.alias('other_bases_units') - parent_units = arel_table.alias('parent_units') + sub_units = arel_table.alias('sub_units') Unit.with(units: self.with_defaults).left_joins(:base) # Exclude Units that are/have default counterpart @@ -32,20 +32,30 @@ class Unit < ApplicationRecord .and(other_units[:symbol].eq(arel_table[:symbol])) .and(other_units[:user_id].not_eq(arel_table[:user_id])) ).exists - # Decide if Unit can be im-/exported based on existing hierarchy - ).joins( - arel_table.create_join(parent_units, - arel_table.create_on( - parent_units[:symbol].eq(bases_units[:symbol]) - .and(parent_units[:user_id].not_eq(bases_units[:user_id])) - ), - Arel::Nodes::OuterJoin) + # Decide if Unit can be im-/exported based on existing hierarchy: + # * same base unit symbol has to exist + # * unit with subunits can only be ported to root ).select( arel_table[Arel.star], - Arel::Nodes::IsNotDistinctFrom.new(parent_units[:symbol], bases_units[:symbol]) - .as('portable') + arel_table[:base_id].eq(nil).or( + ( + Arel::SelectManager.new.project(1).from(other_units) + .join(sub_units).on(other_units[:id].eq(sub_units[:base_id])) + .where( + other_units[:symbol].eq(arel_table[:symbol]) + .and(other_units[:user_id].not_eq(arel_table[:user_id])) + ) + .exists.not + ).and( + Arel::SelectManager.new.project(1).from(other_bases_units) + .where( + other_bases_units[:symbol].eq(bases_units[:symbol]) + .and(other_bases_units[:user_id].not_eq(bases_units[:user_id])) + ) + .exists + ) + ).as('portable') ) - # complete portability check with children } scope :ordered, ->{ left_outer_joins(:base) diff --git a/app/views/default/units/_unit.html.erb b/app/views/default/units/_unit.html.erb index 53d8889..19af7a3 100644 --- a/app/views/default/units/_unit.html.erb +++ b/app/views/default/units/_unit.html.erb @@ -1,16 +1,17 @@ <%= tag.tr do %> - + <%= unit.symbol %> <% if current_user.at_least(:active) && unit.default? %> <%= image_button_to t('.import'), 'download-outline', import_default_unit_path(unit), - disabled: !unit.portable? %> + !unit.portable? ? {disabled: true, aria: {disabled: true}, tabindex: -1} : {} %> <% end %> <% if current_user.at_least(:admin) %> <% if !unit.default? %> - <%= image_button_to t('.export'), 'upload-outline', export_default_unit_path(unit) %> + <%= image_button_to t('.export'), 'upload-outline', export_default_unit_path(unit), + !unit.portable? ? {disabled: true, aria: {disabled: true}, tabindex: -1} : {} %> <% else %> <%= image_button_to t('.delete'), 'delete-outline', unit_path(unit), method: :delete %> -- 2.49.1 From f0e28deea2af45ee403dbb8dd69799ee1b864f2e Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 17 Nov 2024 03:39:39 +0100 Subject: [PATCH 19/53] Implement 'import' action --- app/controllers/default/units_controller.rb | 17 ++++++++++++++++- app/models/unit.rb | 10 +++++----- app/views/default/units/index.turbo_stream.erb | 3 +++ 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 app/views/default/units/index.turbo_stream.erb diff --git a/app/controllers/default/units_controller.rb b/app/controllers/default/units_controller.rb index ea47bbb..e65b8fa 100644 --- a/app/controllers/default/units_controller.rb +++ b/app/controllers/default/units_controller.rb @@ -1,7 +1,8 @@ class Default::UnitsController < ApplicationController navigation_tab :units - before_action :find_unit, only: [:import, :export, :destroy] + before_action -> { find_unit(current_user) }, only: :export + before_action -> { find_unit(nil) }, only: [:import, :destroy] before_action except: :index do case action_name.to_sym @@ -17,9 +18,16 @@ class Default::UnitsController < ApplicationController end def import + current_user.units + .find_or_initialize_by(symbol: @unit.symbol) + .update!(base: @base, **@unit.slice(:name, :multiplier)) + run_and_render :index end def import_all + # From defaults_diff return not only portability, but reason for not being + # portable: missing_base and nesting_too_deep. Add portable and + # missing_base, if possible in one query end def export @@ -27,4 +35,11 @@ class Default::UnitsController < ApplicationController def destroy end + + private + + def find_unit(user) + @unit = Unit.find_by!(id: params[:id], user: user) + @base = Unit.find_by!(symbol: @unit.base.symbol, user: user? ? nil : user) if @unit.base + end end diff --git a/app/models/unit.rb b/app/models/unit.rb index 53af7df..ece321b 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -28,9 +28,9 @@ class Unit < ApplicationRecord .outer_join(other_bases_units) .on(other_units[:base_id].eq(other_bases_units[:id])) .where( - other_bases_units[:symbol].eq(bases_units[:symbol]) + other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol]) .and(other_units[:symbol].eq(arel_table[:symbol])) - .and(other_units[:user_id].not_eq(arel_table[:user_id])) + .and(other_units[:user_id].is_distinct_from(arel_table[:user_id])) ).exists # Decide if Unit can be im-/exported based on existing hierarchy: # * same base unit symbol has to exist @@ -43,14 +43,14 @@ class Unit < ApplicationRecord .join(sub_units).on(other_units[:id].eq(sub_units[:base_id])) .where( other_units[:symbol].eq(arel_table[:symbol]) - .and(other_units[:user_id].not_eq(arel_table[:user_id])) + .and(other_units[:user_id].is_distinct_from(arel_table[:user_id])) ) .exists.not ).and( Arel::SelectManager.new.project(1).from(other_bases_units) .where( - other_bases_units[:symbol].eq(bases_units[:symbol]) - .and(other_bases_units[:user_id].not_eq(bases_units[:user_id])) + other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol]) + .and(other_bases_units[:user_id].is_distinct_from(bases_units[:user_id])) ) .exists ) diff --git a/app/views/default/units/index.turbo_stream.erb b/app/views/default/units/index.turbo_stream.erb new file mode 100644 index 0000000..2628fa7 --- /dev/null +++ b/app/views/default/units/index.turbo_stream.erb @@ -0,0 +1,3 @@ +<%= turbo_stream.update :units do %> + <%= render(@units) || render_no_items %> +<% end %> -- 2.49.1 From 1790f2e7f22d17fa3df0b54698378ae78cb663b2 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Tue, 19 Nov 2024 00:00:18 +0100 Subject: [PATCH 20/53] Rails upgrade to 7.2.2 to enable recursive CTEs --- Gemfile | 2 +- Gemfile.lock | 238 +++++++++++++++++++++++++-------------------------- 2 files changed, 120 insertions(+), 120 deletions(-) diff --git a/Gemfile b/Gemfile index 75a875e..ee86856 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" ruby file: ".ruby-version" -gem "rails", "~> 7.1.2" +gem "rails", "~> 7.2.2" gem "sprockets-rails" gem "mysql2", "~> 0.5" gem "puma", "~> 6.0" diff --git a/Gemfile.lock b/Gemfile.lock index e6c06b6..c891546 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,124 +1,122 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.1.3) - actionpack (= 7.1.3) - activesupport (= 7.1.3) + actioncable (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.3) - actionpack (= 7.1.3) - activejob (= 7.1.3) - activerecord (= 7.1.3) - activestorage (= 7.1.3) - activesupport (= 7.1.3) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.3) - actionpack (= 7.1.3) - actionview (= 7.1.3) - activejob (= 7.1.3) - activesupport (= 7.1.3) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp + actionmailbox (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) + mail (>= 2.8.0) + actionmailer (7.2.2) + actionpack (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activesupport (= 7.2.2) + mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.1.3) - actionview (= 7.1.3) - activesupport (= 7.1.3) + actionpack (7.2.2) + actionview (= 7.2.2) + activesupport (= 7.2.2) nokogiri (>= 1.8.5) racc - rack (>= 2.2.4) + rack (>= 2.2.4, < 3.2) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.3) - actionpack (= 7.1.3) - activerecord (= 7.1.3) - activestorage (= 7.1.3) - activesupport (= 7.1.3) + useragent (~> 0.16) + actiontext (7.2.2) + actionpack (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.3) - activesupport (= 7.1.3) + actionview (7.2.2) + activesupport (= 7.2.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.1.3) - activesupport (= 7.1.3) + activejob (7.2.2) + activesupport (= 7.2.2) globalid (>= 0.3.6) - activemodel (7.1.3) - activesupport (= 7.1.3) - activerecord (7.1.3) - activemodel (= 7.1.3) - activesupport (= 7.1.3) + activemodel (7.2.2) + activesupport (= 7.2.2) + activerecord (7.2.2) + activemodel (= 7.2.2) + activesupport (= 7.2.2) timeout (>= 0.4.0) - activestorage (7.1.3) - actionpack (= 7.1.3) - activejob (= 7.1.3) - activerecord (= 7.1.3) - activesupport (= 7.1.3) + activestorage (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activesupport (= 7.2.2) marcel (~> 1.0) - activesupport (7.1.3) + activesupport (7.2.2) base64 + benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) base64 (0.2.0) bcrypt (3.1.20) - bigdecimal (3.1.6) + benchmark (0.4.0) + bigdecimal (3.1.8) bindex (0.8.1) - builder (3.2.4) + builder (3.3.0) byebug (11.1.3) - capybara (3.39.2) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.4) connection_pool (2.4.1) crass (1.0.6) - date (3.3.4) - devise (4.9.3) + date (3.4.0) + devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - drb (2.2.0) - ruby2_keywords - erubi (1.12.0) - ffi (1.16.3) + drb (2.2.1) + erubi (1.13.0) + ffi (1.17.0-x86_64-linux-gnu) globalid (1.2.1) activesupport (>= 6.1) - i18n (1.14.1) + i18n (1.14.6) concurrent-ruby (~> 1.0) - importmap-rails (2.0.1) + importmap-rails (2.0.3) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) io-console (0.7.2) - irb (1.11.1) - rdoc + irb (1.14.1) + rdoc (>= 4.0.0) reline (>= 0.4.2) - loofah (2.22.0) + logger (1.6.1) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -126,53 +124,51 @@ GEM net-imap net-pop net-smtp - marcel (1.0.2) + marcel (1.0.4) matrix (0.4.2) mini_mime (1.1.5) - minitest (5.21.2) - mutex_m (0.2.0) - mysql2 (0.5.5) - net-imap (0.4.9.1) + minitest (5.25.1) + mysql2 (0.5.6) + net-imap (0.5.1) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-smtp (0.4.0.1) + net-smtp (0.5.0) net-protocol - nio4r (2.7.0) - nokogiri (1.16.0-x86_64-linux) + nio4r (2.7.4) + nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) - psych (5.1.2) + psych (5.2.0) stringio - public_suffix (5.0.4) - puma (6.4.2) + public_suffix (6.0.1) + puma (6.4.3) nio4r (~> 2.0) - racc (1.7.3) - rack (3.0.8) + racc (1.8.1) + rack (3.1.8) rack-session (2.0.0) rack (>= 3.0.0) rack-test (2.1.0) rack (>= 1.3) - rackup (2.1.0) + rackup (2.2.1) rack (>= 3) - webrick (~> 1.8) - rails (7.1.3) - actioncable (= 7.1.3) - actionmailbox (= 7.1.3) - actionmailer (= 7.1.3) - actionpack (= 7.1.3) - actiontext (= 7.1.3) - actionview (= 7.1.3) - activejob (= 7.1.3) - activemodel (= 7.1.3) - activerecord (= 7.1.3) - activestorage (= 7.1.3) - activesupport (= 7.1.3) + rails (7.2.2) + actioncable (= 7.2.2) + actionmailbox (= 7.2.2) + actionmailer (= 7.2.2) + actionpack (= 7.2.2) + actiontext (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activemodel (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) bundler (>= 1.15.0) - railties (= 7.1.3) + railties (= 7.2.2) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -180,25 +176,24 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.1.3) - actionpack (= 7.1.3) - activesupport (= 7.1.3) - irb + railties (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) + irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) - rake (13.1.0) - rdoc (6.6.2) + rake (13.2.1) + rdoc (6.7.0) psych (>= 4.0.0) - regexp_parser (2.9.0) - reline (0.4.2) + regexp_parser (2.9.2) + reline (0.5.11) io-console (~> 0.5) responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.2.6) - ruby2_keywords (0.0.5) + rexml (3.3.9) rubyzip (2.3.2) sassc (2.4.0) ffi (~> 1.9) @@ -208,27 +203,30 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (4.16.0) + securerandom (0.3.2) + selenium-webdriver (4.26.0) + base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - stringio (3.1.0) - thor (1.3.0) - tilt (2.3.0) - timeout (0.4.1) - turbo-rails (2.0.0.pre.beta.3) + stringio (3.1.2) + thor (1.3.2) + tilt (2.4.0) + timeout (0.4.2) + turbo-rails (2.0.11) actionpack (>= 6.0.0) - activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + useragent (0.16.10) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) @@ -236,14 +234,13 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webrick (1.8.1) - websocket (1.2.10) + websocket (1.2.11) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.12) + zeitwerk (2.7.1) PLATFORMS x86_64-linux @@ -255,7 +252,7 @@ DEPENDENCIES importmap-rails mysql2 (~> 0.5) puma (~> 6.0) - rails (~> 7.1.2) + rails (~> 7.2.2) sassc-rails selenium-webdriver sprockets-rails @@ -263,5 +260,8 @@ DEPENDENCIES tzinfo-data web-console +RUBY VERSION + ruby 3.3.0p0 + BUNDLED WITH 2.5.3 -- 2.49.1 From 6c678b65607ccf804f5807a0f62c73331c1a4144 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Thu, 21 Nov 2024 01:50:29 +0100 Subject: [PATCH 21/53] defaults_diff returns base Units where needed --- app/models/unit.rb | 81 +++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index ece321b..872d0b7 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -14,48 +14,57 @@ class Unit < ApplicationRecord validates :multiplier, numericality: {other_than: 0}, if: :base scope :defaults, ->{ where(user: nil) } - scope :with_defaults, ->{ self.or(Unit.where(user: nil)) } scope :defaults_diff, ->{ + actionable_units = Arel::Table.new('actionable_units') + units = actionable_units.alias('units') bases_units = arel_table.alias('bases_units') other_units = arel_table.alias('other_units') other_bases_units = arel_table.alias('other_bases_units') sub_units = arel_table.alias('sub_units') - Unit.with(units: self.with_defaults).left_joins(:base) - # Exclude Units that are/have default counterpart - .where.not( - Arel::SelectManager.new.project(1).from(other_units) - .outer_join(other_bases_units) - .on(other_units[:base_id].eq(other_bases_units[:id])) - .where( - other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol]) - .and(other_units[:symbol].eq(arel_table[:symbol])) - .and(other_units[:user_id].is_distinct_from(arel_table[:user_id])) - ).exists - # Decide if Unit can be im-/exported based on existing hierarchy: - # * same base unit symbol has to exist - # * unit with subunits can only be ported to root - ).select( - arel_table[Arel.star], - arel_table[:base_id].eq(nil).or( - ( - Arel::SelectManager.new.project(1).from(other_units) - .join(sub_units).on(other_units[:id].eq(sub_units[:base_id])) - .where( - other_units[:symbol].eq(arel_table[:symbol]) - .and(other_units[:user_id].is_distinct_from(arel_table[:user_id])) - ) - .exists.not - ).and( - Arel::SelectManager.new.project(1).from(other_bases_units) - .where( - other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol]) - .and(other_bases_units[:user_id].is_distinct_from(bases_units[:user_id])) - ) - .exists - ) - ).as('portable') - ) + Unit.with_recursive(actionable_units: [ + Unit.with(units: self.or(Unit.defaults)).left_joins(:base) + .where.not( + # Exclude Units that are/have default counterpart + Arel::SelectManager.new.project(1).from(other_units) + .outer_join(other_bases_units) + .on(other_units[:base_id].eq(other_bases_units[:id])) + .where( + other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol]) + .and(other_units[:symbol].eq(arel_table[:symbol])) + .and(other_units[:user_id].is_distinct_from(arel_table[:user_id])) + ).exists + ) + .select( + arel_table[Arel.star], + # Decide if Unit can be im-/exported based on existing hierarchy: + # * same base unit symbol has to exist + # * unit with subunits can only be ported to root + arel_table[:base_id].eq(nil).or( + ( + Arel::SelectManager.new.project(1).from(other_units) + .join(sub_units).on(other_units[:id].eq(sub_units[:base_id])) + .where( + other_units[:symbol].eq(arel_table[:symbol]) + .and(other_units[:user_id].is_distinct_from(arel_table[:user_id])) + ).exists.not + ).and( + Arel::SelectManager.new.project(1).from(other_bases_units) + .where( + other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol]) + .and(other_bases_units[:user_id].is_distinct_from(bases_units[:user_id])) + ).exists + ) + ).as('portable') + ), + # TODO: replace AS and MIN with Arel + # TODO: turn off ONLY_FULL_GROUP_BY + # Add missing base Units. Duplicates will be removed by final group(), as + # actionable Units will differ on 'portable' column and can't be UNION-ed. + arel_table.join(actionable_units).on(actionable_units[:base_id].eq(arel_table[:id])) + .project(arel_table[Arel.star], 'NULL AS portable') + ]).select(units: column_names)#, 'MIN(units.portable)' => :portable) + .from(units).group(Unit.column_names) } scope :ordered, ->{ left_outer_joins(:base) -- 2.49.1 From 279f9bd6aca08216252f185d777ea1a1a08d1407 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Fri, 22 Nov 2024 03:10:08 +0100 Subject: [PATCH 22/53] Display Defaults hierarchy including same base Units --- app/helpers/application_helper.rb | 4 ++++ app/models/unit.rb | 11 +++++------ app/views/default/units/_unit.html.erb | 26 ++++++++++++++------------ 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4007d3a..aac55b6 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -123,6 +123,10 @@ module ApplicationHelper "Turbo.renderStreamMessage('#{j(render partial: partial, locals: locals)}'); return false;" end + def disabled_attributes(disabled) + disabled ? {disabled: true, aria: {disabled: true}, tabindex: -1} : {} + end + private # Converts value to HTML formatted scientific notation diff --git a/app/models/unit.rb b/app/models/unit.rb index 872d0b7..29c05fb 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -57,13 +57,12 @@ class Unit < ApplicationRecord ) ).as('portable') ), - # TODO: replace AS and MIN with Arel - # TODO: turn off ONLY_FULL_GROUP_BY - # Add missing base Units. Duplicates will be removed by final group(), as - # actionable Units will differ on 'portable' column and can't be UNION-ed. + # NOTE: turn off ONLY_FULL_GROUP_BY or is it incompatible with other DBs? + # Fill base Units to display proper hierarchy. Duplicates will be removed + # by final group() - can't be deduplicated with UNION due to 'portable' field. arel_table.join(actionable_units).on(actionable_units[:base_id].eq(arel_table[:id])) - .project(arel_table[Arel.star], 'NULL AS portable') - ]).select(units: column_names)#, 'MIN(units.portable)' => :portable) + .project(arel_table[Arel.star], Arel::Nodes.build_quoted(nil).as('portable')) + ]).select(units: column_names).select(units[:portable].minimum.as('portable')) .from(units).group(Unit.column_names) } scope :ordered, ->{ diff --git a/app/views/default/units/_unit.html.erb b/app/views/default/units/_unit.html.erb index 19af7a3..1c1ec2d 100644 --- a/app/views/default/units/_unit.html.erb +++ b/app/views/default/units/_unit.html.erb @@ -1,20 +1,22 @@ <%= tag.tr do %> - + <%= unit.symbol %> - <% if current_user.at_least(:active) && unit.default? %> - <%= image_button_to t('.import'), 'download-outline', import_default_unit_path(unit), - !unit.portable? ? {disabled: true, aria: {disabled: true}, tabindex: -1} : {} %> - <% end %> - <% if current_user.at_least(:admin) %> - <% if !unit.default? %> - <%= image_button_to t('.export'), 'upload-outline', export_default_unit_path(unit), - !unit.portable? ? {disabled: true, aria: {disabled: true}, tabindex: -1} : {} %> - <% else %> - <%= image_button_to t('.delete'), 'delete-outline', unit_path(unit), - method: :delete %> + <% unless unit.portable.nil? %> + <% if current_user.at_least(:active) && unit.default? %> + <%= image_button_to t('.import'), 'download-outline', import_default_unit_path(unit), + disabled_attributes(!unit.portable?) %> + <% end %> + <% if current_user.at_least(:admin) %> + <% if unit.default? %> + <%= image_button_to t('.delete'), 'delete-outline', unit_path(unit), method: :delete %> + <% else %> + <%= image_button_to t('.export'), 'upload-outline', export_default_unit_path(unit), + disabled_attributes(!unit.portable?) %> + <% end %> <% end %> <% end %> -- 2.49.1 From e157c17e0e512c7dba42fa6f499dccde81ac06a2 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Fri, 22 Nov 2024 15:03:33 +0100 Subject: [PATCH 23/53] Hide 'Import all' button until implemented --- app/views/default/units/index.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/default/units/index.html.erb b/app/views/default/units/index.html.erb index e25331d..0d5c3d8 100644 --- a/app/views/default/units/index.html.erb +++ b/app/views/default/units/index.html.erb @@ -1,6 +1,7 @@
<% if current_user.at_least(:active) %> - <%= image_link_to t('.import_all'), 'download-multiple-outline', + <%# TODO: implement Import all %> + <%#= image_link_to t('.import_all'), 'download-multiple-outline', import_all_default_units_path, data: {turbo_stream: true} %> <% end %> <%= image_link_to t('.back'), 'arrow-left-bold-outline', units_path, class: 'tools' %> -- 2.49.1 From 1d439928e2fd50427a2a9d67ee49a13e6a1da00c Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Fri, 22 Nov 2024 15:18:27 +0100 Subject: [PATCH 24/53] Import with proper base --- app/controllers/default/units_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/default/units_controller.rb b/app/controllers/default/units_controller.rb index e65b8fa..d117005 100644 --- a/app/controllers/default/units_controller.rb +++ b/app/controllers/default/units_controller.rb @@ -40,6 +40,6 @@ class Default::UnitsController < ApplicationController def find_unit(user) @unit = Unit.find_by!(id: params[:id], user: user) - @base = Unit.find_by!(symbol: @unit.base.symbol, user: user? ? nil : user) if @unit.base + @base = Unit.find_by!(symbol: @unit.base.symbol, user: user ? nil : current_user) if @unit.base end end -- 2.49.1 From bdc4ec46447a5e338519374ac292794e066a5c62 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Fri, 22 Nov 2024 15:48:09 +0100 Subject: [PATCH 25/53] Specify user modifiable ATTRIBUTES --- app/controllers/default/units_controller.rb | 3 ++- app/controllers/units_controller.rb | 2 +- app/models/unit.rb | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/controllers/default/units_controller.rb b/app/controllers/default/units_controller.rb index d117005..9ca24a7 100644 --- a/app/controllers/default/units_controller.rb +++ b/app/controllers/default/units_controller.rb @@ -18,9 +18,10 @@ class Default::UnitsController < ApplicationController end def import + params = @unit.slice(Unit::ATTRIBUTES - [:symbol, :base_id]) current_user.units .find_or_initialize_by(symbol: @unit.symbol) - .update!(base: @base, **@unit.slice(:name, :multiplier)) + .update!(base: @base, **params) run_and_render :index end diff --git a/app/controllers/units_controller.rb b/app/controllers/units_controller.rb index f48cfef..81fdc92 100644 --- a/app/controllers/units_controller.rb +++ b/app/controllers/units_controller.rb @@ -58,7 +58,7 @@ class UnitsController < ApplicationController private def unit_params - params.require(:unit).permit(:symbol, :name, :base_id, :multiplier) + params.require(:unit).permit(Unit::ATTRIBUTES) end def find_unit diff --git a/app/models/unit.rb b/app/models/unit.rb index 29c05fb..b4f3c86 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -1,4 +1,6 @@ class Unit < ApplicationRecord + ATTRIBUTES = [:symbol, :name, :multiplier, :base_id] + belongs_to :user, optional: true belongs_to :base, optional: true, class_name: "Unit" has_many :subunits, class_name: "Unit", dependent: :restrict_with_error, inverse_of: :base -- 2.49.1 From 76ce2eeeddde4be32cb18e25748b336a2ca7c480 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 23 Nov 2024 02:24:08 +0100 Subject: [PATCH 26/53] Display Unit name using #to_s --- app/controllers/units_controller.rb | 2 +- app/models/unit.rb | 4 ++++ app/views/default/units/_unit.html.erb | 2 +- app/views/units/_unit.html.erb | 2 +- config/locales/en.yml | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/units_controller.rb b/app/controllers/units_controller.rb index 81fdc92..1426fca 100644 --- a/app/controllers/units_controller.rb +++ b/app/controllers/units_controller.rb @@ -42,7 +42,7 @@ class UnitsController < ApplicationController permitted = params.require(:unit).permit(:base_id) if permitted[:base_id].blank? && @unit.multiplier != 1 permitted.merge!(multiplier: 1) - flash.now[:notice] = t(".multiplier_reset", symbol: @unit.symbol) + flash.now[:notice] = t(".multiplier_reset", unit: @unit) end run_and_render :index if @unit.update(permitted) diff --git a/app/models/unit.rb b/app/models/unit.rb index b4f3c86..f4131de 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -78,6 +78,10 @@ class Unit < ApplicationRecord nil end + def to_s + symbol + end + def movable? subunits.empty? end diff --git a/app/views/default/units/_unit.html.erb b/app/views/default/units/_unit.html.erb index 1c1ec2d..8a4a6df 100644 --- a/app/views/default/units/_unit.html.erb +++ b/app/views/default/units/_unit.html.erb @@ -1,7 +1,7 @@ <%= tag.tr do %> - <%= unit.symbol %> + <%= unit %> diff --git a/app/views/units/_unit.html.erb b/app/views/units/_unit.html.erb index 7039a05..d04c175 100644 --- a/app/views/units/_unit.html.erb +++ b/app/views/units/_unit.html.erb @@ -5,7 +5,7 @@ data: {drag_path: rebase_unit_path(unit), drop_id: dom_id(unit.base || unit)} do %> - <%= link_to unit.symbol, edit_unit_path(unit), id: dom_id(unit, :edit), + <%= link_to unit, edit_unit_path(unit), id: dom_id(unit, :edit), onclick: 'this.blur();', data: {turbo_stream: true} %> <%= unit.name %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 2e45765..20cb320 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -64,7 +64,7 @@ en: update: success: Updated unit rebase: - multiplier_reset: Multiplier of "%{symbol}" has been reset to 1, due to repositioning + multiplier_reset: Multiplier of "%{unit}" has been reset to 1, due to repositioning destroy: success: Deleted unit default: -- 2.49.1 From e75391ae1894dc553441f97669a4f5c53e8973c0 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 23 Nov 2024 14:55:29 +0100 Subject: [PATCH 27/53] Display User name using #to_s --- app/helpers/application_helper.rb | 2 +- app/models/user.rb | 4 ++++ app/views/layouts/application.html.erb | 2 +- app/views/users/index.html.erb | 2 +- app/views/users/mailer/password_change.html.erb | 2 +- app/views/users/mailer/reset_password_instructions.html.erb | 2 +- app/views/users/mailer/unlock_instructions.html.erb | 2 +- 7 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index aac55b6..a7fa5b0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -84,7 +84,7 @@ module ApplicationHelper [:button_to, :link_to, :link_to_unless_current].each do |method_name| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def image_#{method_name}(name, image = nil, options = nil, html_options = {}, &block) - name = svg_tag("pictograms/\#{image}") + name if image + name = svg_tag("pictograms/\#{image}") + name.to_s if image html_options[:class] = class_names( html_options[:class], diff --git a/app/models/user.rb b/app/models/user.rb index 3c43d9b..49b9b1a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,6 +13,10 @@ class User < ApplicationRecord has_many :units, dependent: :destroy + def to_s + email + end + def at_least(status) User.statuses[self.status] >= User.statuses[status] end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index ff65d54..f1b024a 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -29,7 +29,7 @@ <%= image_link_to t(".issue_tracker"), "bug-outline", issue_tracker_url, class: "extendedright" %> <% if user_signed_in? %> - <%= image_link_to_unless_current(current_user.email, "account-wrench-outline", + <%= image_link_to_unless_current(current_user, "account-wrench-outline", edit_user_registration_path) {} %> <% if current_user_disguised? %> <%= image_link_to t(".revert"), "incognito-off", revert_users_path %> diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 18e8799..26c5502 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -11,7 +11,7 @@ <% @users.each do |user| %> - <%= link_to user.email, user_path(user) %> + <%= link_to user, user_path(user) %> <% if user == current_user %> <%= user.status %> diff --git a/app/views/users/mailer/password_change.html.erb b/app/views/users/mailer/password_change.html.erb index b41daf4..e750ca7 100644 --- a/app/views/users/mailer/password_change.html.erb +++ b/app/views/users/mailer/password_change.html.erb @@ -1,3 +1,3 @@ -

Hello <%= @resource.email %>!

+

Hello <%= @resource %>!

We're contacting you to notify you that your password has been changed.

diff --git a/app/views/users/mailer/reset_password_instructions.html.erb b/app/views/users/mailer/reset_password_instructions.html.erb index f667dc1..f1a48b5 100644 --- a/app/views/users/mailer/reset_password_instructions.html.erb +++ b/app/views/users/mailer/reset_password_instructions.html.erb @@ -1,4 +1,4 @@ -

Hello <%= @resource.email %>!

+

Hello <%= @resource %>!

Someone has requested a link to change your password. You can do this through the link below.

diff --git a/app/views/users/mailer/unlock_instructions.html.erb b/app/views/users/mailer/unlock_instructions.html.erb index 41e148b..0bf243d 100644 --- a/app/views/users/mailer/unlock_instructions.html.erb +++ b/app/views/users/mailer/unlock_instructions.html.erb @@ -1,4 +1,4 @@ -

Hello <%= @resource.email %>!

+

Hello <%= @resource %>!

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

-- 2.49.1 From f4ca1e91fa9fbf4cf23638d37f76ac1e66081364 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 23 Nov 2024 23:26:08 +0100 Subject: [PATCH 28/53] Mark redirecting buttons with trailing '...' Closes #2 --- app/helpers/application_helper.rb | 8 +++++++- app/views/default/units/index.html.erb | 2 +- config/locales/en.yml | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a7fa5b0..4a92ba7 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -84,7 +84,8 @@ module ApplicationHelper [:button_to, :link_to, :link_to_unless_current].each do |method_name| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def image_#{method_name}(name, image = nil, options = nil, html_options = {}, &block) - name = svg_tag("pictograms/\#{image}") + name.to_s if image + name = name.to_s + name = svg_tag("pictograms/\#{image}") + name if image html_options[:class] = class_names( html_options[:class], @@ -95,6 +96,11 @@ module ApplicationHelper html_options[:onclick] = "return confirm('\#{html_options[:onclick][:confirm]}');" end + if __method__.start_with?('image_link_to') && + !(html_options[:onclick] || html_options.dig(:data, :turbo_stream)) + name = name + '...' + end + send :#{method_name}, name, options, html_options, &block end RUBY_EVAL diff --git a/app/views/default/units/index.html.erb b/app/views/default/units/index.html.erb index 0d5c3d8..13c5a3e 100644 --- a/app/views/default/units/index.html.erb +++ b/app/views/default/units/index.html.erb @@ -1,7 +1,7 @@
<% if current_user.at_least(:active) %> <%# TODO: implement Import all %> - <%#= image_link_to t('.import_all'), 'download-multiple-outline', + <%#= image_button_to t('.import_all'), 'download-multiple-outline', import_all_default_units_path, data: {turbo_stream: true} %> <% end %> <%= image_link_to t('.back'), 'arrow-left-bold-outline', units_path, class: 'tools' %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 20cb320..ff3c4fd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -54,7 +54,7 @@ en: delete_unit: Delete index: add_unit: Add unit - import_units: Import... + import_units: Import no_items: There are no configured units. You can try to import some defaults. top_level_drop: Drop here to reposition into top-level unit new: @@ -76,10 +76,10 @@ en: index: actions: Actions on defaults import_all: Import all - back: Back to units... + back: Back to units users: index: - disguise: View as... + disguise: View as passwords: edit: new_password: New password -- 2.49.1 From f3751c5fa137be0d4eafbc455db7ce7e352f8f95 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 23 Nov 2024 23:42:21 +0100 Subject: [PATCH 29/53] Disallow NULLs --- db/migrate/20230602185352_create_units.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/migrate/20230602185352_create_units.rb b/db/migrate/20230602185352_create_units.rb index 9a91f6d..fb878b6 100644 --- a/db/migrate/20230602185352_create_units.rb +++ b/db/migrate/20230602185352_create_units.rb @@ -2,12 +2,12 @@ class CreateUnits < ActiveRecord::Migration[7.0] def change create_table :units do |t| t.references :user, foreign_key: true - t.string :symbol + t.string :symbol, null: false t.string :name - t.decimal :multiplier, precision: 30, scale: 15, default: 1.0 + t.decimal :multiplier, null: false, precision: 30, scale: 15, default: 1.0 t.references :base - t.timestamps + t.timestamps null: false end add_index :units, [:user_id, :symbol], unique: true end -- 2.49.1 From d6fdff252a70604e84d1bdd081a096fe0efdbe69 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 24 Nov 2024 14:11:54 +0100 Subject: [PATCH 30/53] Validate User 'email' and 'unconfirmed_email' lengths Closes #6 --- app/models/user.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 49b9b1a..e929c59 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,6 +13,10 @@ class User < ApplicationRecord has_many :units, dependent: :destroy + validates :email, presence: true, uniqueness: true, + length: {maximum: columns_hash['email'].limit} + validates :unconfirmed_email, length: {maximum: columns_hash['unconfirmed_email'].limit} + def to_s email end -- 2.49.1 From 37112516563d65c5756580d92079a4a4a97a9020 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 24 Nov 2024 15:13:59 +0100 Subject: [PATCH 31/53] Unit: limit symbol length, change name:string -> description:text Closes #11 Closes #12 --- app/assets/stylesheets/application.css | 28 ++++++++++++++++------- app/models/unit.rb | 4 ++-- app/views/units/_form.html.erb | 4 ++-- app/views/units/_unit.html.erb | 2 +- app/views/units/index.html.erb | 2 +- db/migrate/20230602185352_create_units.rb | 4 ++-- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 14ec9a7..797249b 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -98,16 +98,21 @@ input[type=submit] { width: fit-content; } input:not([type=submit]):not([type=checkbox]), -select { +select, +textarea { padding: 0.2em 0.4em; } .button, button, input, -select { +select, +textarea { border: solid 1px var(--color-gray); border-radius: 0.25em; } +textarea { + margin: 0 +} .button > svg, .tab > svg, button > svg { @@ -151,7 +156,8 @@ input[type=checkbox]:checked { -webkit-appearance: checkbox; } input:hover, -select:hover { +select:hover, +textarea:hover { border-color: #009ade; outline: solid 1px #009ade; } @@ -160,11 +166,13 @@ select:hover { } input:focus-visible, select:focus-within, -select:focus-visible { +select:focus-visible, +textarea:focus-visible { accent-color: #006c9b; background-color: var(--color-focus-gray); } -input[type=text]:read-only { +input[type=text]:read-only, +textarea:read-only { border: none; padding-left: 0; padding-right: 0; @@ -336,7 +344,7 @@ table.items th, table.items td { padding-inline: 1em 0; } -table.items td:has(input) { +table.items td:has(input, textarea) { padding-inline-start: calc(0.6em - 0.9px); } table.items th:last-child { @@ -367,7 +375,7 @@ table.items td.link a::after { table.items td.subunit { padding-inline-start: 1.8em; } -table.items td.subunit:has(input) { +table.items td.subunit:has(input, textarea) { padding-inline-start: calc(1.4em - 1px); } table.items td.actions { @@ -390,6 +398,9 @@ table.items tr.dropzone::after { table.items td.handle { cursor: move; } +table.items tr.form td { + vertical-align: top; +} /* TODO: replace :hover:focus-visible combos with proper LOVE stye order */ /* TODO: Update styling, including rem removal. */ @@ -440,7 +451,8 @@ table.items input[type=submit] { table.items .button:not(:hover), table.items button:not(:hover), table.items input:not(:hover), -table.items select:not(:hover) { +table.items select:not(:hover), +table.items textarea:not(:hover) { border-color: var(--color-border-gray); } table.items .button:not(:hover), diff --git a/app/models/unit.rb b/app/models/unit.rb index f4131de..de362aa 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -1,5 +1,5 @@ class Unit < ApplicationRecord - ATTRIBUTES = [:symbol, :name, :multiplier, :base_id] + ATTRIBUTES = [:symbol, :description, :multiplier, :base_id] belongs_to :user, optional: true belongs_to :base, optional: true, class_name: "Unit" @@ -11,7 +11,7 @@ class Unit < ApplicationRecord end validates :symbol, presence: true, uniqueness: {scope: :user_id}, length: {maximum: columns_hash['symbol'].limit} - validates :name, length: {maximum: columns_hash['name'].limit} + validates :description, length: {maximum: columns_hash['description'].limit} validates :multiplier, numericality: {equal_to: 1}, unless: :base validates :multiplier, numericality: {other_than: 0}, if: :base diff --git a/app/views/units/_form.html.erb b/app/views/units/_form.html.erb index e2dd699..3d4dfc6 100644 --- a/app/views/units/_form.html.erb +++ b/app/views/units/_form.html.erb @@ -7,8 +7,8 @@ maxlength: @unit.class.columns_hash['symbol'].limit, autocomplete: "off" %> - <%= form.text_field :name, form: :unit_form, size: 30, - maxlength: @unit.class.columns_hash['name'].limit, autocomplete: "off" %> + <%= form.text_area :description, form: :unit_form, cols: 30, rows: 1, escape: false, + maxlength: @unit.class.columns_hash['description'].limit, autocomplete: "off" %> <% unless @unit.base.nil? %> diff --git a/app/views/units/_unit.html.erb b/app/views/units/_unit.html.erb index d04c175..d1eec64 100644 --- a/app/views/units/_unit.html.erb +++ b/app/views/units/_unit.html.erb @@ -8,7 +8,7 @@ <%= link_to unit, edit_unit_path(unit), id: dom_id(unit, :edit), onclick: 'this.blur();', data: {turbo_stream: true} %> - <%= unit.name %> + <%= unit.description %> <%= scientifize(unit.multiplier) %> <% if current_user.at_least(:active) %> diff --git a/app/views/units/index.html.erb b/app/views/units/index.html.erb index c6d4aa2..e2b8258 100644 --- a/app/views/units/index.html.erb +++ b/app/views/units/index.html.erb @@ -12,7 +12,7 @@ <%= User.human_attribute_name(:symbol).capitalize %> - <%= User.human_attribute_name(:name).capitalize %> + <%= User.human_attribute_name(:description).capitalize %> <%= User.human_attribute_name(:multiplier).capitalize %> <% if current_user.at_least(:active) %> <%= t :actions %> diff --git a/db/migrate/20230602185352_create_units.rb b/db/migrate/20230602185352_create_units.rb index fb878b6..4b5f64c 100644 --- a/db/migrate/20230602185352_create_units.rb +++ b/db/migrate/20230602185352_create_units.rb @@ -2,8 +2,8 @@ class CreateUnits < ActiveRecord::Migration[7.0] def change create_table :units do |t| t.references :user, foreign_key: true - t.string :symbol, null: false - t.string :name + t.string :symbol, null: false, limit: 15 + t.text :description t.decimal :multiplier, null: false, precision: 30, scale: 15, default: 1.0 t.references :base -- 2.49.1 From f9bd81c6abfbd02371262008f81c794090fda718 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Tue, 26 Nov 2024 02:31:25 +0100 Subject: [PATCH 32/53] Implement Unit defaults export Disable import_all until implemented --- app/controllers/default/units_controller.rb | 31 +++++++++------------ app/models/unit.rb | 10 +++++-- config/routes.rb | 2 +- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/app/controllers/default/units_controller.rb b/app/controllers/default/units_controller.rb index 9ca24a7..e6dd2ed 100644 --- a/app/controllers/default/units_controller.rb +++ b/app/controllers/default/units_controller.rb @@ -1,16 +1,13 @@ class Default::UnitsController < ApplicationController navigation_tab :units - before_action -> { find_unit(current_user) }, only: :export - before_action -> { find_unit(nil) }, only: [:import, :destroy] + before_action :find_unit, only: [:import, :export, :destroy] - before_action except: :index do - case action_name.to_sym - when :import, :import_all - raise AccessForbidden unless current_user.at_least(:active) - else - raise AccessForbidden unless current_user.at_least(:admin) - end + before_action only: :import do + raise AccessForbidden unless current_user.at_least(:active) + end + before_action except: [:index, :import] do + raise AccessForbidden unless current_user.at_least(:admin) end def index @@ -18,20 +15,19 @@ class Default::UnitsController < ApplicationController end def import - params = @unit.slice(Unit::ATTRIBUTES - [:symbol, :base_id]) - current_user.units - .find_or_initialize_by(symbol: @unit.symbol) - .update!(base: @base, **params) + raise ParameterInvalid unless @unit.default? && @unit.port(current_user) run_and_render :index end - def import_all + #def import_all # From defaults_diff return not only portability, but reason for not being # portable: missing_base and nesting_too_deep. Add portable and # missing_base, if possible in one query - end + #end def export + raise ParameterInvalid unless !@unit.default? && @unit.port(nil) + run_and_render :index end def destroy @@ -39,8 +35,7 @@ class Default::UnitsController < ApplicationController private - def find_unit(user) - @unit = Unit.find_by!(id: params[:id], user: user) - @base = Unit.find_by!(symbol: @unit.base.symbol, user: user ? nil : current_user) if @unit.base + def find_unit + @unit = Unit.find_by!(id: params[:id], user: [current_user, nil]) end end diff --git a/app/models/unit.rb b/app/models/unit.rb index de362aa..82fcd7f 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -24,6 +24,8 @@ class Unit < ApplicationRecord other_bases_units = arel_table.alias('other_bases_units') sub_units = arel_table.alias('sub_units') + # TODO: move inner 'with' CTE to outer 'with recursive' - it can have multiple + # CTEs, even non recursive ones. Unit.with_recursive(actionable_units: [ Unit.with(units: self.or(Unit.defaults)).left_joins(:base) .where.not( @@ -90,7 +92,11 @@ class Unit < ApplicationRecord user_id.nil? end - def exportable? - !default? && (base.nil? || base.default?) + def port(recipient) + recipient_base = base && Unit.find_by(symbol: base.symbol, user: recipient) + return nil if recipient_base.nil? != base.nil? + params = slice(ATTRIBUTES - [:symbol, :base_id]) + Unit.find_or_initialize_by(user: recipient, symbol: symbol) + .update(base: recipient_base, **params) end end diff --git a/config/routes.rb b/config/routes.rb index 06c1241..b62b98d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,7 +9,7 @@ Rails.application.routes.draw do namespace :default do resources :units, only: [:index, :destroy] do member { post :import, :export } - collection { post :import_all } + #collection { post :import_all } end end -- 2.49.1 From 1fedd70fe59da3ab7fc93f09a597d018a75d8d46 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Wed, 27 Nov 2024 19:54:50 +0100 Subject: [PATCH 33/53] Fix defaults listing Base symbol was displayed twice when it existed as default and non-default and both of them had at least one subunit. Also: sorting by base_id yielded non-alphabetic order in such case. --- app/models/unit.rb | 13 +++++++++---- app/views/default/units/_unit.html.erb | 3 +-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index 82fcd7f..fbb2c8e 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -61,18 +61,23 @@ class Unit < ApplicationRecord ) ).as('portable') ), - # NOTE: turn off ONLY_FULL_GROUP_BY or is it incompatible with other DBs? # Fill base Units to display proper hierarchy. Duplicates will be removed # by final group() - can't be deduplicated with UNION due to 'portable' field. arel_table.join(actionable_units).on(actionable_units[:base_id].eq(arel_table[:id])) .project(arel_table[Arel.star], Arel::Nodes.build_quoted(nil).as('portable')) - ]).select(units: column_names).select(units[:portable].minimum.as('portable')) - .from(units).group(Unit.column_names) + ]).select(units: [:base_id, :symbol]) + .select( + units[:id].minimum.as('id'), # can be ANY_VALUE() + units[:user_id].minimum.as('user_id'), # prefer non-default + Arel::Nodes.build_quoted(1).as('multiplier'), # disregard multiplier when sorting + units[:portable].minimum.as('portable') + ) + .from(units).group(:base_id, :symbol) } scope :ordered, ->{ left_outer_joins(:base) .order(arel_table.coalesce(Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]), - arel_table[:base_id].asc.nulls_first, :multiplier, :symbol) + arel_table[:base_id].not_eq(nil), :multiplier, :symbol) } before_destroy do diff --git a/app/views/default/units/_unit.html.erb b/app/views/default/units/_unit.html.erb index 8a4a6df..13c2d9e 100644 --- a/app/views/default/units/_unit.html.erb +++ b/app/views/default/units/_unit.html.erb @@ -1,6 +1,5 @@ <%= tag.tr do %> - + <%= unit %> -- 2.49.1 From 2cbae12fa24dd28c8d13560a22420506ebe84220 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 30 Nov 2024 16:11:31 +0100 Subject: [PATCH 34/53] Implement Units default destroy --- app/controllers/default/units_controller.rb | 21 +++++++++++++++++---- app/models/unit.rb | 8 ++++---- app/views/default/units/_unit.html.erb | 3 ++- config/locales/en.yml | 9 +++++++++ 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/app/controllers/default/units_controller.rb b/app/controllers/default/units_controller.rb index e6dd2ed..6ca2733 100644 --- a/app/controllers/default/units_controller.rb +++ b/app/controllers/default/units_controller.rb @@ -1,7 +1,8 @@ class Default::UnitsController < ApplicationController navigation_tab :units - before_action :find_unit, only: [:import, :export, :destroy] + before_action :find_unit, only: :export + before_action :find_unit_default, only: [:import, :destroy] before_action only: :import do raise AccessForbidden unless current_user.at_least(:active) @@ -15,7 +16,9 @@ class Default::UnitsController < ApplicationController end def import - raise ParameterInvalid unless @unit.default? && @unit.port(current_user) + @unit.port!(current_user) + flash.now[:notice] = t('.success', unit: @unit) + ensure run_and_render :index end @@ -26,16 +29,26 @@ class Default::UnitsController < ApplicationController #end def export - raise ParameterInvalid unless !@unit.default? && @unit.port(nil) + @unit.port!(nil) + flash.now[:notice] = t('.success', unit: @unit) + ensure run_and_render :index end def destroy + @unit.destroy! + flash.now[:notice] = t('.success', unit: @unit) + ensure + run_and_render :index end private def find_unit - @unit = Unit.find_by!(id: params[:id], user: [current_user, nil]) + @unit = Unit.find_by!(id: params[:id], user: current_user) + end + + def find_unit_default + @unit = Unit.find_by!(id: params[:id], user: nil) end end diff --git a/app/models/unit.rb b/app/models/unit.rb index fbb2c8e..027c492 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -97,11 +97,11 @@ class Unit < ApplicationRecord user_id.nil? end - def port(recipient) - recipient_base = base && Unit.find_by(symbol: base.symbol, user: recipient) - return nil if recipient_base.nil? != base.nil? + # Should only by invoked on Units returned from #defaults_diff which are #portable + def port!(recipient) + recipient_base = base && Unit.find_by!(symbol: base.symbol, user: recipient) params = slice(ATTRIBUTES - [:symbol, :base_id]) Unit.find_or_initialize_by(user: recipient, symbol: symbol) - .update(base: recipient_base, **params) + .update!(base: recipient_base, **params) end end diff --git a/app/views/default/units/_unit.html.erb b/app/views/default/units/_unit.html.erb index 13c2d9e..83e605b 100644 --- a/app/views/default/units/_unit.html.erb +++ b/app/views/default/units/_unit.html.erb @@ -11,7 +11,8 @@ <% end %> <% if current_user.at_least(:admin) %> <% if unit.default? %> - <%= image_button_to t('.delete'), 'delete-outline', unit_path(unit), method: :delete %> + <%= image_button_to t('.delete'), 'delete-outline', default_unit_path(unit), + method: :delete %> <% else %> <%= image_button_to t('.export'), 'upload-outline', export_default_unit_path(unit), disabled_attributes(!unit.portable?) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index ff3c4fd..fe127bb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -33,6 +33,9 @@ en: forbidden: > You have not been granted access to this action (403 Forbidden). This should not happen, please notify site administrator. + not_found: > + The record that you requested operation on does not exist (404). + This should not happen, please notify site administrator. unprocessable_entity: > The request is semantically incorrect and was rejected (422 Unprocessable Entity). This should not happen, please notify site administrator. @@ -77,6 +80,12 @@ en: actions: Actions on defaults import_all: Import all back: Back to units + import: + success: Imported unit "%{unit}" + export: + success: Exported unit "%{unit}" + destroy: + success: Deleted unit "%{unit}" users: index: disguise: View as -- 2.49.1 From 13685aa476a98426fe3c4943e1b269e9332a7b2c Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 30 Nov 2024 16:28:43 +0100 Subject: [PATCH 35/53] Update error handling according to new rules --- app/controllers/units_controller.rb | 21 ++++++++++++--------- config/locales/en.yml | 6 +++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/controllers/units_controller.rb b/app/controllers/units_controller.rb index 1426fca..87c67ea 100644 --- a/app/controllers/units_controller.rb +++ b/app/controllers/units_controller.rb @@ -19,7 +19,7 @@ class UnitsController < ApplicationController def create @unit = current_user.units.new(unit_params) if @unit.save - flash.now[:notice] = t(".success") + flash.now[:notice] = t('.success', unit: @unit) run_and_render :index else render :new @@ -31,7 +31,7 @@ class UnitsController < ApplicationController def update if @unit.update(unit_params.except(:base_id)) - flash.now[:notice] = t(".success") + flash.now[:notice] = t('.success', unit: @unit) run_and_render :index else render :edit @@ -40,18 +40,21 @@ class UnitsController < ApplicationController def rebase permitted = params.require(:unit).permit(:base_id) - if permitted[:base_id].blank? && @unit.multiplier != 1 - permitted.merge!(multiplier: 1) + permitted.merge!(multiplier: 1) if permitted[:base_id].blank? && @unit.multiplier != 1 + + @unit.update!(permitted) + + if @unit.multiplier_previously_changed? flash.now[:notice] = t(".multiplier_reset", unit: @unit) end - - run_and_render :index if @unit.update(permitted) + ensure + run_and_render :index end def destroy - if @unit.destroy - flash.now[:notice] = t(".success") - end + @unit.destroy! + flash.now[:notice] = t('.success', unit: @unit) + ensure run_and_render :index end diff --git a/config/locales/en.yml b/config/locales/en.yml index fe127bb..31dd9a9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -63,13 +63,13 @@ en: new: none: none create: - success: Created new unit + success: Created new unit "%{unit}" update: - success: Updated unit + success: Updated unit "%{unit}" rebase: multiplier_reset: Multiplier of "%{unit}" has been reset to 1, due to repositioning destroy: - success: Deleted unit + success: Deleted unit "%{unit}" default: units: unit: -- 2.49.1 From b38d72e9b07f4bcf06eba5051cd3028cad6d87a4 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 30 Nov 2024 20:15:30 +0100 Subject: [PATCH 36/53] Return to per-action permission filters --- app/controllers/users_controller.rb | 12 +++++------- config/locales/en.yml | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0472ccf..4bf7ac4 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,13 +3,11 @@ class UsersController < ApplicationController before_action :find_user, only: [:show, :update, :disguise] - before_action do - case action_name.to_sym - when :revert - raise AccessForbidden unless current_user_disguised? - else - raise AccessForbidden unless current_user.at_least(:admin) - end + before_action only: :revert do + raise AccessForbidden unless current_user_disguised? + end + before_action except: :revert do + raise AccessForbidden unless current_user.at_least(:admin) end def index diff --git a/config/locales/en.yml b/config/locales/en.yml index 31dd9a9..1ba2a21 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -34,7 +34,7 @@ en: You have not been granted access to this action (403 Forbidden). This should not happen, please notify site administrator. not_found: > - The record that you requested operation on does not exist (404). + The record that you requested operation on does not exist (404 Not Found). This should not happen, please notify site administrator. unprocessable_entity: > The request is semantically incorrect and was rejected (422 Unprocessable Entity). -- 2.49.1 From 2e4eb3d4b5dc037334d9ead5233d27c618b2ff6e Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Fri, 6 Dec 2024 01:17:05 +0100 Subject: [PATCH 37/53] Rake task to export default settings as seeds --- README.md | 9 +++++++ config/initializers/core_ext.rb | 1 + db/seeds.rb | 17 +----------- db/seeds/templates/units.erb | 9 +++++++ db/seeds/units.rb | 36 ++++++++++++++++++++++++++ lib/core_ext/big_decimal/formatting.rb | 14 ++++++++++ lib/tasks/db.rake | 12 +++++++++ 7 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 config/initializers/core_ext.rb create mode 100644 db/seeds/templates/units.erb create mode 100644 db/seeds/units.rb create mode 100644 lib/core_ext/big_decimal/formatting.rb create mode 100644 lib/tasks/db.rake diff --git a/README.md b/README.md index c5c1087..138e1d5 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,15 @@ Tests need to be run from within toplevel application directory: bundle exec rails test test/system/users_test.rb --seed 1234 + ### Icons Pictogrammers Material Design Icons: https://pictogrammers.com/library/mdi/ + + +### Rake tasks + +Exporting default settings defined in application to seed file (e.g. to send as +PR or share between installations): + + bundle exec rails db:seed:export diff --git a/config/initializers/core_ext.rb b/config/initializers/core_ext.rb new file mode 100644 index 0000000..5feeadb --- /dev/null +++ b/config/initializers/core_ext.rb @@ -0,0 +1 @@ +require 'core_ext/big_decimal/formatting' diff --git a/db/seeds.rb b/db/seeds.rb index ff9e6e0..335a35f 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -20,19 +20,4 @@ end # Formulas will be deleted as dependent on Quantities #[Source, Quantity, Unit].each { |model| model.defaults.delete_all } -Unit.transaction do - Unit.defaults.delete_all - - unit_1 = Unit.create symbol: "1", name: "dimensionless, one" - Unit.create symbol: "%", base: unit_1, multiplier: 1e-2, name: "percent" - Unit.create symbol: "‰", base: unit_1, multiplier: 1e-3, name: "promille" - Unit.create symbol: "‱", base: unit_1, multiplier: 1e-4, name: "basis point" - Unit.create symbol: "ppm", base: unit_1, multiplier: 1e-6, name: "parts per million" - - unit_g = Unit.create symbol: "g", name: "gram" - Unit.create symbol: "ug", base: unit_g, multiplier: 1e-6, name: "microgram" - Unit.create symbol: "mg", base: unit_g, multiplier: 1e-3, name: "milligram" - Unit.create symbol: "kg", base: unit_g, multiplier: 1e3, name: "kilogram" - - Unit.create symbol: "kcal", name: "kilocalorie" -end +require 'seeds/units.rb' diff --git a/db/seeds/templates/units.erb b/db/seeds/templates/units.erb new file mode 100644 index 0000000..c2d4f88 --- /dev/null +++ b/db/seeds/templates/units.erb @@ -0,0 +1,9 @@ +Unit.transaction do + Unit.defaults.delete_all +<% Unit.defaults.ordered.each do |unit| %> +<%= "\n" if unit.base.nil? %> + unit_<%= unit.symbol %> = + Unit.create symbol: "<%= unit.symbol %>",<% unless unit.base.nil? %> base: unit_<%= unit.base.symbol %>, multiplier: <%= unit.multiplier.to_scientific %>,<% end %> + description: "<%= unit.description %>" +<% end %> +end diff --git a/db/seeds/units.rb b/db/seeds/units.rb new file mode 100644 index 0000000..1b79bf9 --- /dev/null +++ b/db/seeds/units.rb @@ -0,0 +1,36 @@ +Unit.transaction do + Unit.defaults.delete_all + + unit_1 = + Unit.create symbol: "1", + description: "dimensionless, one" + unit_ppm = + Unit.create symbol: "ppm", base: unit_1, multiplier: 1e-6, + description: "parts per million" + unit_‱ = + Unit.create symbol: "‱", base: unit_1, multiplier: 1e-4, + description: "basis point" + unit_‰ = + Unit.create symbol: "‰", base: unit_1, multiplier: 1e-3, + description: "promille" + unit_% = + Unit.create symbol: "%", base: unit_1, multiplier: 1e-2, + description: "percent" + + unit_g = + Unit.create symbol: "g", + description: "gram" + unit_ug = + Unit.create symbol: "ug", base: unit_g, multiplier: 1e-6, + description: "microgram" + unit_mg = + Unit.create symbol: "mg", base: unit_g, multiplier: 1e-3, + description: "milligram" + unit_kg = + Unit.create symbol: "kg", base: unit_g, multiplier: 1e3, + description: "kilogram" + + unit_kcal = + Unit.create symbol: "kcal", + description: "kilocalorie" +end diff --git a/lib/core_ext/big_decimal/formatting.rb b/lib/core_ext/big_decimal/formatting.rb new file mode 100644 index 0000000..ed9bbce --- /dev/null +++ b/lib/core_ext/big_decimal/formatting.rb @@ -0,0 +1,14 @@ +module FixinMe + module BigDecimalScientificNotation + def to_scientific + return 'NaN' unless finite? + + sign, coefficient, base, exponent = split + (sign == -1 ? '-' : '') + + (coefficient.length > 1 ? coefficient.insert(1, '.') : coefficient) + + (exponent != 1 ? "e#{exponent-1}" : '') + end + end +end + +BigDecimal.prepend(FixinMe::BigDecimalScientificNotation) diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake new file mode 100644 index 0000000..f2e5233 --- /dev/null +++ b/lib/tasks/db.rake @@ -0,0 +1,12 @@ +namespace :db do + namespace :seed do + desc "Dump default settings as seed data to db/seeds/*.rb" + task export: :environment do + seeds_path = Pathname.new(Rails.application.paths["db"].first) / 'seeds' + (seeds_path / 'templates').glob('*.erb').each do |template_path| + template = ERB.new(template_path.read, trim_mode: '<>') + (seeds_path / "#{template_path.basename('.*').to_s}.rb").write(template.result) + end + end + end +end -- 2.49.1 From 7e5f873cde86944e08586ddcc873733420e1b0a1 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Fri, 6 Dec 2024 15:24:25 +0100 Subject: [PATCH 38/53] Change helper into BigDecimal method --- app/helpers/application_helper.rb | 24 ------------------------ app/views/units/_unit.html.erb | 2 +- lib/core_ext/big_decimal/formatting.rb | 22 ++++++++++++++++++++++ 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4a92ba7..1f87cfc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -132,28 +132,4 @@ module ApplicationHelper def disabled_attributes(disabled) disabled ? {disabled: true, aria: {disabled: true}, tabindex: -1} : {} end - - private - - # Converts value to HTML formatted scientific notation - def scientifize(d) - sign, coefficient, base, exponent = d.split - return 'NaN' unless sign - - result = (sign == -1 ? '-' : '') - unless coefficient == '1' && sign == 1 - if coefficient.length > 1 - result += coefficient.insert(1, '.') - elsif - result += coefficient - end - if exponent != 1 - result += "×" - end - end - if exponent != 1 - result += "10% d" % [exponent-1] - end - result.html_safe - end end diff --git a/app/views/units/_unit.html.erb b/app/views/units/_unit.html.erb index d1eec64..dda3faa 100644 --- a/app/views/units/_unit.html.erb +++ b/app/views/units/_unit.html.erb @@ -9,7 +9,7 @@ onclick: 'this.blur();', data: {turbo_stream: true} %> <%= unit.description %> - <%= scientifize(unit.multiplier) %> + <%= unit.multiplier.to_html %> <% if current_user.at_least(:active) %> diff --git a/lib/core_ext/big_decimal/formatting.rb b/lib/core_ext/big_decimal/formatting.rb index ed9bbce..8418326 100644 --- a/lib/core_ext/big_decimal/formatting.rb +++ b/lib/core_ext/big_decimal/formatting.rb @@ -8,6 +8,28 @@ module FixinMe (coefficient.length > 1 ? coefficient.insert(1, '.') : coefficient) + (exponent != 1 ? "e#{exponent-1}" : '') end + + # Converts value to HTML formatted scientific notation + def to_html + sign, coefficient, base, exponent = split + return 'NaN' unless sign + + result = (sign == -1 ? '-' : '') + unless coefficient == '1' && sign == 1 + if coefficient.length > 1 + result += coefficient.insert(1, '.') + elsif + result += coefficient + end + if exponent != 1 + result += "×" + end + end + if exponent != 1 + result += "10% d" % [exponent-1] + end + result.html_safe + end end end -- 2.49.1 From d31ff5442ab6552844c333d093be06e8410ae77b Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Fri, 6 Dec 2024 15:29:03 +0100 Subject: [PATCH 39/53] Update messages for empty Unit/default Unit indexes Closes #26 --- config/locales/en.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 1ba2a21..2bce615 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -58,7 +58,7 @@ en: index: add_unit: Add unit import_units: Import - no_items: There are no configured units. You can try to import some defaults. + no_items: There are no configured units. You can Add some or Import from defaults. top_level_drop: Drop here to reposition into top-level unit new: none: none @@ -78,8 +78,9 @@ en: import: Import index: actions: Actions on defaults - import_all: Import all back: Back to units + import_all: Import all + no_items: There are no differences between default and user units. import: success: Imported unit "%{unit}" export: -- 2.49.1 From 25ac126df95a83cede16d4f30b39c37b46b7f215 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 7 Dec 2024 16:05:07 +0100 Subject: [PATCH 40/53] Systematize core extesions --- config/initializers/core_ext.rb | 6 +++++- config/initializers/system_test_case.rb | 3 --- ...t_helper_unique_id.rb => screenshot_helper_unique_id.rb} | 2 +- .../formatting.rb => big_decimal_scientific_notation.rb} | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 config/initializers/system_test_case.rb rename lib/core_ext/action_dispatch/system_testing/test_helpers/{custom_screenshot_helper_unique_id.rb => screenshot_helper_unique_id.rb} (52%) rename lib/core_ext/{big_decimal/formatting.rb => big_decimal_scientific_notation.rb} (92%) diff --git a/config/initializers/core_ext.rb b/config/initializers/core_ext.rb index 5feeadb..54a1409 100644 --- a/config/initializers/core_ext.rb +++ b/config/initializers/core_ext.rb @@ -1 +1,5 @@ -require 'core_ext/big_decimal/formatting' +require 'core_ext/big_decimal_scientific_notation' + +ActiveSupport.on_load :action_dispatch_system_test_case do + prepend CoreExt::ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelperUniqueId +end diff --git a/config/initializers/system_test_case.rb b/config/initializers/system_test_case.rb deleted file mode 100644 index 9e3ef71..0000000 --- a/config/initializers/system_test_case.rb +++ /dev/null @@ -1,3 +0,0 @@ -ActiveSupport.on_load :action_dispatch_system_test_case do - prepend CoreExt::ActionDispatch::SystemTesting::TestHelpers::CustomScreenshotHelperUniqueId -end diff --git a/lib/core_ext/action_dispatch/system_testing/test_helpers/custom_screenshot_helper_unique_id.rb b/lib/core_ext/action_dispatch/system_testing/test_helpers/screenshot_helper_unique_id.rb similarity index 52% rename from lib/core_ext/action_dispatch/system_testing/test_helpers/custom_screenshot_helper_unique_id.rb rename to lib/core_ext/action_dispatch/system_testing/test_helpers/screenshot_helper_unique_id.rb index 3798c38..248a6fd 100644 --- a/lib/core_ext/action_dispatch/system_testing/test_helpers/custom_screenshot_helper_unique_id.rb +++ b/lib/core_ext/action_dispatch/system_testing/test_helpers/screenshot_helper_unique_id.rb @@ -1,4 +1,4 @@ -module CoreExt::ActionDispatch::SystemTesting::TestHelpers::CustomScreenshotHelperUniqueId +module CoreExt::ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelperUniqueId private def unique diff --git a/lib/core_ext/big_decimal/formatting.rb b/lib/core_ext/big_decimal_scientific_notation.rb similarity index 92% rename from lib/core_ext/big_decimal/formatting.rb rename to lib/core_ext/big_decimal_scientific_notation.rb index 8418326..0a45c90 100644 --- a/lib/core_ext/big_decimal/formatting.rb +++ b/lib/core_ext/big_decimal_scientific_notation.rb @@ -1,4 +1,4 @@ -module FixinMe +module CoreExt module BigDecimalScientificNotation def to_scientific return 'NaN' unless finite? @@ -33,4 +33,4 @@ module FixinMe end end -BigDecimal.prepend(FixinMe::BigDecimalScientificNotation) +BigDecimal.prepend CoreExt::BigDecimalScientificNotation -- 2.49.1 From 15a5515c996a3f6134ef7425944df1b98c221e87 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 7 Dec 2024 20:16:43 +0100 Subject: [PATCH 41/53] Extend NumericalityValidator to check precision and scale Use new checks on Unit.multiplier Closes #28 --- app/models/unit.rb | 2 +- app/views/units/_form.html.erb | 3 ++- config/initializers/core_ext.rb | 4 ++++ config/locales/en.yml | 4 ++++ ...numericality_validates_precision_and_scale.rb | 16 ++++++++++++++++ 5 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 lib/core_ext/active_model/validations/numericality_validates_precision_and_scale.rb diff --git a/app/models/unit.rb b/app/models/unit.rb index 027c492..c40753c 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -13,7 +13,7 @@ class Unit < ApplicationRecord length: {maximum: columns_hash['symbol'].limit} validates :description, length: {maximum: columns_hash['description'].limit} validates :multiplier, numericality: {equal_to: 1}, unless: :base - validates :multiplier, numericality: {other_than: 0}, if: :base + validates :multiplier, numericality: {other_than: 0, precision: true, scale: true}, if: :base scope :defaults, ->{ where(user: nil) } scope :defaults_diff, ->{ diff --git a/app/views/units/_form.html.erb b/app/views/units/_form.html.erb index 3d4dfc6..cc77f6a 100644 --- a/app/views/units/_form.html.erb +++ b/app/views/units/_form.html.erb @@ -13,7 +13,8 @@ <% unless @unit.base.nil? %> <%= form.hidden_field :base_id, form: :unit_form %> - <%= form.number_field :multiplier, form: :unit_form, required: true, step: "any", + <%= form.number_field :multiplier, form: :unit_form, required: true, + step: BigDecimal(10).power(-@unit.class.type_for_attribute(:multiplier).scale), size: 10, autocomplete: "off" %> <% end %> diff --git a/config/initializers/core_ext.rb b/config/initializers/core_ext.rb index 54a1409..fe6bd62 100644 --- a/config/initializers/core_ext.rb +++ b/config/initializers/core_ext.rb @@ -1,5 +1,9 @@ require 'core_ext/big_decimal_scientific_notation' +ActiveSupport.on_load :active_record do + ActiveModel::Validations::NumericalityValidator.prepend CoreExt::ActiveModel::Validations::NumericalityValidatesPrecisionAndScale +end + ActiveSupport.on_load :action_dispatch_system_test_case do prepend CoreExt::ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelperUniqueId end diff --git a/config/locales/en.yml b/config/locales/en.yml index 2bce615..1fa61c1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,4 +1,8 @@ en: + errors: + messages: + precision_exceeded: must not exceed %{value} significant digits + scale_exceeded: must not exceed %{value} decimal digits activerecord: attributes: unit: diff --git a/lib/core_ext/active_model/validations/numericality_validates_precision_and_scale.rb b/lib/core_ext/active_model/validations/numericality_validates_precision_and_scale.rb new file mode 100644 index 0000000..a8fe744 --- /dev/null +++ b/lib/core_ext/active_model/validations/numericality_validates_precision_and_scale.rb @@ -0,0 +1,16 @@ +module CoreExt::ActiveModel::Validations::NumericalityValidatesPrecisionAndScale + def validate_each(record, attr_name, value, ...) + super(record, attr_name, value, ...) + + if options[:precision] || options[:scale] + attr_type = record.class.type_for_attribute(attr_name) + value = BigDecimal(value) unless value.is_a? BigDecimal + if options[:precision] && (value.precision > attr_type.precision) + record.errors.add(attr_name, :precision_exceeded, **filtered_options(attr_type.precision)) + end + if options[:scale] && (value.scale > attr_type.scale) + record.errors.add(attr_name, :scale_exceeded, **filtered_options(attr_type.scale)) + end + end + end +end -- 2.49.1 From 0b201606c29e8c37fe02d62cec179bb8a72588ec Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 8 Dec 2024 13:47:30 +0100 Subject: [PATCH 42/53] Replace #columns_hash with #type_for_attribute for limits --- app/models/unit.rb | 4 ++-- app/models/user.rb | 4 ++-- app/views/units/_form.html.erb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index c40753c..d10de22 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -10,8 +10,8 @@ class Unit < ApplicationRecord errors.add(:base, :multilevel_nesting) if base.base.present? end validates :symbol, presence: true, uniqueness: {scope: :user_id}, - length: {maximum: columns_hash['symbol'].limit} - validates :description, length: {maximum: columns_hash['description'].limit} + length: {maximum: type_for_attribute(:symbol).limit} + validates :description, length: {maximum: type_for_attribute(:description).limit} validates :multiplier, numericality: {equal_to: 1}, unless: :base validates :multiplier, numericality: {other_than: 0, precision: true, scale: true}, if: :base diff --git a/app/models/user.rb b/app/models/user.rb index e929c59..35f8c76 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -14,8 +14,8 @@ class User < ApplicationRecord has_many :units, dependent: :destroy validates :email, presence: true, uniqueness: true, - length: {maximum: columns_hash['email'].limit} - validates :unconfirmed_email, length: {maximum: columns_hash['unconfirmed_email'].limit} + length: {maximum: type_for_attribute(:email).limit} + validates :unconfirmed_email, length: {maximum: type_for_attribute(:unconfirmed_email).limit} def to_s email diff --git a/app/views/units/_form.html.erb b/app/views/units/_form.html.erb index cc77f6a..f59b3a8 100644 --- a/app/views/units/_form.html.erb +++ b/app/views/units/_form.html.erb @@ -4,11 +4,11 @@ <%= form.text_field :symbol, form: :unit_form, required: true, autofocus: true, size: 12, - maxlength: @unit.class.columns_hash['symbol'].limit, autocomplete: "off" %> + maxlength: @unit.class.type_for_attribute(:symbol).limit, autocomplete: "off" %> <%= form.text_area :description, form: :unit_form, cols: 30, rows: 1, escape: false, - maxlength: @unit.class.columns_hash['description'].limit, autocomplete: "off" %> + maxlength: @unit.class.type_for_attribute(:description).limit, autocomplete: "off" %> <% unless @unit.base.nil? %> -- 2.49.1 From e49eac766caf12877d1cf4d7e101cc12be4aee21 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 8 Dec 2024 14:25:06 +0100 Subject: [PATCH 43/53] Add decimal type requirements --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 138e1d5..d2420f5 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,11 @@ Software requirements * Server side: * Ruby version: developed on Ruby 3.x - * database with recursive Common Table Expressions (CTE) support, e.g. - MySQL >= 8.0, MariaDB >= 10.2.2 + * database with: + * recursive Common Table Expressions (CTE) support, e.g. + MySQL >= 8.0, MariaDB >= 10.2.2 + * decimal type with precision of at least 30 (not sure if SQLite3 + supports this) * for testing: browser as specified in _Client side_ requirements * Client side: * browser supporting below requirements (e.g. Firefox >= 121): -- 2.49.1 From a18f257378ec3d09a0517058b1760b9d52171e22 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 8 Dec 2024 15:28:38 +0100 Subject: [PATCH 44/53] Schema update --- db/schema.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 1dddba7..0006b01 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,12 +10,12 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2023_06_02_185352) do +ActiveRecord::Schema[7.2].define(version: 2023_06_02_185352) do create_table "units", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "user_id" - t.string "symbol" - t.string "name" - t.decimal "multiplier", precision: 30, scale: 15, default: "1.0" + t.string "symbol", limit: 15, null: false + t.text "description" + t.decimal "multiplier", precision: 30, scale: 15, default: "1.0", null: false t.bigint "base_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false -- 2.49.1 From 40808639ccd35b7cd35586f0ef07455f5e44b25d Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 8 Dec 2024 15:29:01 +0100 Subject: [PATCH 45/53] Update tests to new schema --- test/application_system_test_case.rb | 4 +++- test/fixtures/units.yml | 16 ++++++++-------- test/system/units_test.rb | 7 ++++--- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index fab452d..e641868 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -4,7 +4,9 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase include ActionView::Helpers::UrlHelper # NOTE: geckodriver installed with Firefox, ignore incompatibility warning - Selenium::WebDriver.logger.ignore(:selenium_manager) + Selenium::WebDriver.logger + .ignore(:selenium_manager, :clear_session_storage, :clear_local_storage) + Capybara.configure do |config| config.save_path = "#{Rails.root}/tmp/screenshots/" end diff --git a/test/fixtures/units.yml b/test/fixtures/units.yml index d580608..6e6ca5b 100644 --- a/test/fixtures/units.yml +++ b/test/fixtures/units.yml @@ -1,40 +1,40 @@ g: user: admin symbol: g - name: gram + description: gram kg: user: admin symbol: kg - name: kilogram + description: kilogram multiplier: 1000 base: g 1: user: admin symbol: 1 - name: one + description: one s: user: admin symbol: s - name: second + description: second percent: user: admin symbol: '%' - name: percent + description: percent multiplier: 0.01 base: 1 µg: user: admin symbol: µg - name: microgram + description: microgram multiplier: 0.000001 base: g mg: user: admin symbol: mg - name: milligram + description: milligram multiplier: 0.001 base: g g_alice: user: alice symbol: g - name: gram + description: gram diff --git a/test/system/units_test.rb b/test/system/units_test.rb index c96127e..67d415b 100644 --- a/test/system/units_test.rb +++ b/test/system/units_test.rb @@ -20,16 +20,17 @@ class UnitsTest < ApplicationSystemTestCase end end + # TODO: extend with add subunit test "add unit" do click_on t('units.index.add_unit') within 'tbody > tr:has(input[type=text], textarea)' do assert_selector ':focus' - maxlength = all(:fillable_field).to_h { |f| [f[:name], f[:maxlength].to_i || 1000] } + maxlength = all(:fillable_field).to_h { |f| [f[:description], f[:maxlength].to_i || 1000] } fill_in 'unit[symbol]', with: SecureRandom.random_symbol(rand([1..15, 15..maxlength['unit[symbol]']].sample)) - fill_in 'unit[name]', - with: [nil, SecureRandom.alphanumeric(rand(1..maxlength['unit[name]']))].sample + fill_in 'unit[description]', + with: [nil, SecureRandom.alphanumeric(rand(1..maxlength['unit[description]']))].sample assert_difference ->{ Unit.count }, 1 do click_on t('helpers.submit.create') end -- 2.49.1 From f3f0b9dc9e24a182dca5a812694c238643e62646 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Mon, 9 Dec 2024 20:00:14 +0100 Subject: [PATCH 46/53] Fix UnitsiTest#test_index --- app/models/unit.rb | 2 +- test/system/units_test.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index d10de22..c4445b2 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -3,7 +3,7 @@ class Unit < ApplicationRecord belongs_to :user, optional: true belongs_to :base, optional: true, class_name: "Unit" - has_many :subunits, class_name: "Unit", dependent: :restrict_with_error, inverse_of: :base + has_many :subunits, class_name: "Unit", inverse_of: :base, dependent: :restrict_with_error validate if: ->{ base.present? } do errors.add(:base, :user_mismatch) unless user == base.user diff --git a/test/system/units_test.rb b/test/system/units_test.rb index 67d415b..a67a629 100644 --- a/test/system/units_test.rb +++ b/test/system/units_test.rb @@ -12,7 +12,8 @@ class UnitsTest < ApplicationSystemTestCase assert_selector 'tr', count: @user.units.count end - Unit.destroy_all + # Cannot #destroy_all due to {dependent: :restrict*} on Unit.subunits association + @user.units.delete_all visit units_path within 'tbody' do assert_selector 'tr', count: 1 -- 2.49.1 From 2bbf62b84b826ee346a11ded4cdb946f7c258b79 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Mon, 9 Dec 2024 20:01:04 +0100 Subject: [PATCH 47/53] Update gems --- Gemfile.lock | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c891546..49c2bbf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -93,7 +93,7 @@ GEM concurrent-ruby (1.3.4) connection_pool (2.4.1) crass (1.0.6) - date (3.4.0) + date (3.4.1) devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -111,11 +111,11 @@ GEM actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) - io-console (0.7.2) + io-console (0.8.0) irb (1.14.1) rdoc (>= 4.0.0) reline (>= 0.4.2) - logger (1.6.1) + logger (1.6.2) loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -127,7 +127,7 @@ GEM marcel (1.0.4) matrix (0.4.2) mini_mime (1.1.5) - minitest (5.25.1) + minitest (5.25.4) mysql2 (0.5.6) net-imap (0.5.1) date @@ -139,13 +139,14 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.4) - nokogiri (1.16.7-x86_64-linux) + nokogiri (1.16.8-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) - psych (5.2.0) + psych (5.2.1) + date stringio public_suffix (6.0.1) - puma (6.4.3) + puma (6.5.0) nio4r (~> 2.0) racc (1.8.1) rack (3.1.8) @@ -173,9 +174,9 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.1) loofah (~> 2.21) - nokogiri (~> 1.14) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) railties (7.2.2) actionpack (= 7.2.2) activesupport (= 7.2.2) @@ -185,10 +186,10 @@ GEM thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rake (13.2.1) - rdoc (6.7.0) + rdoc (6.8.1) psych (>= 4.0.0) - regexp_parser (2.9.2) - reline (0.5.11) + regexp_parser (2.9.3) + reline (0.5.12) io-console (~> 0.5) responders (3.1.1) actionpack (>= 5.2) @@ -203,8 +204,8 @@ GEM sprockets (> 3.0) sprockets-rails tilt - securerandom (0.3.2) - selenium-webdriver (4.26.0) + securerandom (0.4.0) + selenium-webdriver (4.27.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -226,7 +227,7 @@ GEM railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - useragent (0.16.10) + useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) -- 2.49.1 From dc92a333be3029d9f7bee301788cd85e5ebc45d9 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Tue, 10 Dec 2024 01:18:27 +0100 Subject: [PATCH 48/53] Fix UnitsTest#test_add_unit --- test/system/units_test.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/system/units_test.rb b/test/system/units_test.rb index a67a629..745a20e 100644 --- a/test/system/units_test.rb +++ b/test/system/units_test.rb @@ -21,13 +21,14 @@ class UnitsTest < ApplicationSystemTestCase end end + # TODO: check if Add buton is properly disabled/enabled # TODO: extend with add subunit test "add unit" do click_on t('units.index.add_unit') within 'tbody > tr:has(input[type=text], textarea)' do assert_selector ':focus' - maxlength = all(:fillable_field).to_h { |f| [f[:description], f[:maxlength].to_i || 1000] } + maxlength = all(:fillable_field).to_h { |f| [f[:name], f[:maxlength].to_i || 1000] } fill_in 'unit[symbol]', with: SecureRandom.random_symbol(rand([1..15, 15..maxlength['unit[symbol]']].sample)) fill_in 'unit[description]', @@ -41,7 +42,11 @@ class UnitsTest < ApplicationSystemTestCase assert_no_selector :fillable_field assert_selector 'tr', count: @user.units.count end - assert_selector '.flash.notice', text: /^#{t('units.create.success')}/ + assert_selector '.flash.notice', text: /^#{t('units.create.success', unit: @user.units.last)}/ + end + + # TODO: check proper form/button redisplay and flash messages + test "add unit on validation error" do end test "add and edit disallow opening multiple forms" do -- 2.49.1 From bb4fbb3adc342fbb6aaaaa81e1f9085143a7cb28 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Tue, 10 Dec 2024 18:11:13 +0100 Subject: [PATCH 49/53] Refine "add and edit disallow opening multiple forms" test --- test/application_system_test_case.rb | 9 +++++++++ test/system/units_test.rb | 26 ++++++++++---------------- test/test_helper.rb | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index e641868..9372b8e 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -1,6 +1,7 @@ require "test_helper" class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + extend ActionView::Helpers::TranslationHelper include ActionView::Helpers::UrlHelper # NOTE: geckodriver installed with Firefox, ignore incompatibility warning @@ -32,4 +33,12 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase #def assert_stale(element) # assert_raises(Selenium::WebDriver::Error::StaleElementReferenceError) { element.tag_name } #end + + test "click disabled link" do + # Link should be unclickable + # assert_raises(Selenium::WebDriver::Error::ElementClickInterceptedError) do + # # Use custom selector for disabled links + # find('a[disabled]').click + # end + end end diff --git a/test/system/units_test.rb b/test/system/units_test.rb index 745a20e..dbf6025 100644 --- a/test/system/units_test.rb +++ b/test/system/units_test.rb @@ -1,6 +1,8 @@ require "application_system_test_case" class UnitsTest < ApplicationSystemTestCase + ADD_UNIT_LABELS = [t('units.index.add_unit'), t('units.unit.add_subunit')] + setup do @user = sign_in visit units_path @@ -49,36 +51,28 @@ class UnitsTest < ApplicationSystemTestCase test "add unit on validation error" do end + # TODO: add non-empty form closing warning test "add and edit disallow opening multiple forms" do - # Once new/edit form is open, other actions on the same page either replace - # the form or leave it untouched - # TODO: add non-empty form closing warning + # Once new/edit form is open, attempt to open another one will close it links = {} - link_labels = {1 => [t('units.index.add_unit'), t('units.unit.add_subunit')], - 0 => units.map(&:symbol)} + # Define tr count change depending on link clicked + link_labels = {1 => ADD_UNIT_LABELS, 0 => units.map(&:symbol)} link_labels.each_pair do |row_change, labels| all(:link_or_button, exact_text: Regexp.union(labels)).map { |l| links[l] = row_change } end + link, rows = links.assoc(links.keys.sample).tap { |l, _| links.delete(l) } assert_difference ->{ all('tbody tr').count }, rows do link.click end - find 'tbody tr:has(input[type=text]:focus)' - - # Link should be now unavailable or unclickable - begin - assert_raises(Selenium::WebDriver::Error::ElementClickInterceptedError) do - link.click - end if link.visible? - rescue Selenium::WebDriver::Error::StaleElementReferenceError - link = nil - end + find('tbody tr:has(input[type=text])').assert_selector ':focus' + assert !link.visible? || link[:disabled] link = links.keys.select(&:visible?).sample assert_difference ->{ all('tbody tr').count }, links[link] - rows do link.click end - assert_selector 'tbody tr:has(input[type=text]:focus)', count: 1 + find('tbody tr:has(input[type=text])').assert_selector ':focus' end # NOTE: extend with any add/edit link diff --git a/test/test_helper.rb b/test/test_helper.rb index 1e7f8dc..63bc8df 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,8 +9,8 @@ class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all - include AbstractController::Translation include ActionMailer::TestHelper + include ActionView::Helpers::TranslationHelper # NOTE: use public #alphanumeric(chars: ...) from Ruby 3.3 onwards SecureRandom.class_eval do -- 2.49.1 From 3a25c1dbd0f15919f53573c08ac6b619d06dfd2c Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Thu, 12 Dec 2024 00:35:11 +0100 Subject: [PATCH 50/53] Equally sample add unit/add subunit/edit links for test --- test/system/units_test.rb | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/test/system/units_test.rb b/test/system/units_test.rb index dbf6025..0a92352 100644 --- a/test/system/units_test.rb +++ b/test/system/units_test.rb @@ -55,24 +55,39 @@ class UnitsTest < ApplicationSystemTestCase test "add and edit disallow opening multiple forms" do # Once new/edit form is open, attempt to open another one will close it links = {} - # Define tr count change depending on link clicked - link_labels = {1 => ADD_UNIT_LABELS, 0 => units.map(&:symbol)} - link_labels.each_pair do |row_change, labels| - all(:link_or_button, exact_text: Regexp.union(labels)).map { |l| links[l] = row_change } + targets = {} + { + add_unit: t('units.index.add_unit'), + add_subunit: t('units.unit.add_subunit'), + edit: Regexp.union(units.map(&:symbol)) + }.each_pair do |type, labels| + links[type] = all(:link_or_button, exact_text: labels).to_a + targets[type] = links[type].sample end + # Define tr count change depending on link clicked + row_change = {add_unit: 1, add_subunit: 1, edit: 0} - link, rows = links.assoc(links.keys.sample).tap { |l, _| links.delete(l) } + type, link = targets.assoc(targets.keys.sample).tap { |t, _| targets.delete(t) } + rows = row_change[type] assert_difference ->{ all('tbody tr').count }, rows do link.click end - find('tbody tr:has(input[type=text])').assert_selector ':focus' - assert !link.visible? || link[:disabled] + within('tbody tr:has(input[type=text])') { assert_selector ':focus' } + if type == :edit + assert !link.visible? + [:add_subunit, :edit].each do |t| + assert_difference(->{ links[t].length }, -1) { links[t].select!(&:visible?) } + end + else + assert link[:disabled] + end - link = links.keys.select(&:visible?).sample - assert_difference ->{ all('tbody tr').count }, links[link] - rows do + targets.merge([:add_subunit, :edit].map { |t| [t, links[t].sample] }.to_h) + type, link = targets.assoc(targets.keys.sample) + assert_difference ->{ all('tbody tr').count }, row_change[type] - rows do link.click end - find('tbody tr:has(input[type=text])').assert_selector ':focus' + within('tbody tr:has(input[type=text])') { assert_selector ':focus' } end # NOTE: extend with any add/edit link -- 2.49.1 From d5719b1e9d615923216e03067bec78e380cc9f7a Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Thu, 12 Dec 2024 00:44:26 +0100 Subject: [PATCH 51/53] Fill multiplier field, confirm Add button disabled --- app/helpers/application_helper.rb | 6 ++++++ app/views/units/_form.html.erb | 4 ++-- test/application_system_test_case.rb | 6 ++++++ test/system/units_test.rb | 18 +++++++++++++----- test/test_helper.rb | 12 ++++++++++++ 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1f87cfc..4acb234 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -132,4 +132,10 @@ module ApplicationHelper def disabled_attributes(disabled) disabled ? {disabled: true, aria: {disabled: true}, tabindex: -1} : {} end + + def number_attributes(type) + step = BigDecimal(10).power(-type.scale) + max = BigDecimal(10).power(type.precision - type.scale) - step + {min: -max, max: max, step: step} + end end diff --git a/app/views/units/_form.html.erb b/app/views/units/_form.html.erb index f59b3a8..f02f404 100644 --- a/app/views/units/_form.html.erb +++ b/app/views/units/_form.html.erb @@ -14,8 +14,8 @@ <% unless @unit.base.nil? %> <%= form.hidden_field :base_id, form: :unit_form %> <%= form.number_field :multiplier, form: :unit_form, required: true, - step: BigDecimal(10).power(-@unit.class.type_for_attribute(:multiplier).scale), - size: 10, autocomplete: "off" %> + size: 10, autocomplete: "off", + **number_attributes(@unit.class.type_for_attribute(:multiplier)) %> <% end %> diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 9372b8e..2f70892 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -34,6 +34,12 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase # assert_raises(Selenium::WebDriver::Error::StaleElementReferenceError) { element.tag_name } #end + # HTML does not allow [disabled] attribute on tag, so it's not possible to + # easily find them using e.g. :link selector + #Capybara.add_selector(:disabled_link) do + # label " tag with [disabled] attribute" + #end + test "click disabled link" do # Link should be unclickable # assert_raises(Selenium::WebDriver::Error::ElementClickInterceptedError) do diff --git a/test/system/units_test.rb b/test/system/units_test.rb index 0a92352..4c51a22 100644 --- a/test/system/units_test.rb +++ b/test/system/units_test.rb @@ -23,18 +23,24 @@ class UnitsTest < ApplicationSystemTestCase end end - # TODO: check if Add buton is properly disabled/enabled - # TODO: extend with add subunit test "add unit" do - click_on t('units.index.add_unit') + add_link = all(:link, exact_text: ADD_UNIT_LABELS.sample).sample + add_link.click + assert_equal 'disabled', add_link[:disabled] within 'tbody > tr:has(input[type=text], textarea)' do assert_selector ':focus' - maxlength = all(:fillable_field).to_h { |f| [f[:name], f[:maxlength].to_i || 1000] } + + maxlength = all(:fillable_field).to_h { |f| [f[:name], f[:maxlength].to_i || 2**16] } + fill_in 'unit[symbol]', with: SecureRandom.random_symbol(rand([1..15, 15..maxlength['unit[symbol]']].sample)) fill_in 'unit[description]', with: [nil, SecureRandom.alphanumeric(rand(1..maxlength['unit[description]']))].sample + within :field, 'unit[multiplier]' do |field| + fill_in with: random_number(field[:max], field[:step]) + end if add_link[:text] != t('units.index.add_unit') + assert_difference ->{ Unit.count }, 1 do click_on t('helpers.submit.create') end @@ -44,7 +50,9 @@ class UnitsTest < ApplicationSystemTestCase assert_no_selector :fillable_field assert_selector 'tr', count: @user.units.count end - assert_selector '.flash.notice', text: /^#{t('units.create.success', unit: @user.units.last)}/ + assert_no_selector :element, :a, 'disabled': 'disabled', + exact_text: Regexp.union(ADD_UNIT_LABELS) + assert_selector '.flash.notice', text: t('units.create.success', unit: @user.units.last.symbol) end # TODO: check proper form/button redisplay and flash messages diff --git a/test/test_helper.rb b/test/test_helper.rb index 63bc8df..2bb8f4f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -32,6 +32,18 @@ class ActiveSupport::TestCase "%s@%s.%s" % (1..3).map { SecureRandom.alphanumeric(rand(1..20)) } end + # Assumes: max >= step and step = 1e[-]N, both as strings + def random_number(max, step) + max.delete!('.') + precision = max.length + start = rand(precision) + 1 + d = (rand(max.to_i) + 1) % 10**start + length = rand([0, 1..4, 4..precision].sample) + d = d.truncate(-start + length) + d = 10**(start - length) if d.zero? + BigDecimal(step) * d + end + def with_last_email yield(ActionMailer::Base.deliveries.last) end -- 2.49.1 From dcffa86e93c32d3a351da18b0050dd99bab28240 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Thu, 12 Dec 2024 00:53:23 +0100 Subject: [PATCH 52/53] Rename add -> new in test descriptions --- test/system/units_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/system/units_test.rb b/test/system/units_test.rb index 4c51a22..79803f8 100644 --- a/test/system/units_test.rb +++ b/test/system/units_test.rb @@ -23,7 +23,7 @@ class UnitsTest < ApplicationSystemTestCase end end - test "add unit" do + test "new" do add_link = all(:link, exact_text: ADD_UNIT_LABELS.sample).sample add_link.click assert_equal 'disabled', add_link[:disabled] @@ -55,12 +55,12 @@ class UnitsTest < ApplicationSystemTestCase assert_selector '.flash.notice', text: t('units.create.success', unit: @user.units.last.symbol) end - # TODO: check proper form/button redisplay and flash messages - test "add unit on validation error" do + # TODO: check proper form/button redisplay and flash messages on add/edit + test "new and edit form on validation error" do end # TODO: add non-empty form closing warning - test "add and edit disallow opening multiple forms" do + test "new and edit disallow opening multiple forms" do # Once new/edit form is open, attempt to open another one will close it links = {} targets = {} -- 2.49.1 From 30ee4a861e38538c158b51298dce25fee176603e Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 14 Dec 2024 19:40:01 +0100 Subject: [PATCH 53/53] Define LINK_LABELS once. Generate Unicode random strings. --- test/system/units_test.rb | 26 +++++++++++---------- test/test_helper.rb | 48 +++++++++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/test/system/units_test.rb b/test/system/units_test.rb index 79803f8..cbdd625 100644 --- a/test/system/units_test.rb +++ b/test/system/units_test.rb @@ -1,10 +1,16 @@ require "application_system_test_case" class UnitsTest < ApplicationSystemTestCase - ADD_UNIT_LABELS = [t('units.index.add_unit'), t('units.unit.add_subunit')] + LINK_LABELS = { + add_unit: t('units.index.add_unit'), + add_subunit: t('units.unit.add_subunit'), + edit: nil + } setup do @user = sign_in + LINK_LABELS[:edit] = Regexp.union(@user.units.map(&:symbol)) + visit units_path end @@ -24,7 +30,8 @@ class UnitsTest < ApplicationSystemTestCase end test "new" do - add_link = all(:link, exact_text: ADD_UNIT_LABELS.sample).sample + type, label = LINK_LABELS.assoc([:add_unit, :add_subunit].sample) + add_link = all(:link, exact_text: label).sample add_link.click assert_equal 'disabled', add_link[:disabled] @@ -34,9 +41,8 @@ class UnitsTest < ApplicationSystemTestCase maxlength = all(:fillable_field).to_h { |f| [f[:name], f[:maxlength].to_i || 2**16] } fill_in 'unit[symbol]', - with: SecureRandom.random_symbol(rand([1..15, 15..maxlength['unit[symbol]']].sample)) - fill_in 'unit[description]', - with: [nil, SecureRandom.alphanumeric(rand(1..maxlength['unit[description]']))].sample + with: random_string(rand([1..3, 4..maxlength['unit[symbol]']].sample)) + fill_in 'unit[description]', with: random_string(rand(0..maxlength['unit[description]'])) within :field, 'unit[multiplier]' do |field| fill_in with: random_number(field[:max], field[:step]) end if add_link[:text] != t('units.index.add_unit') @@ -51,8 +57,8 @@ class UnitsTest < ApplicationSystemTestCase assert_selector 'tr', count: @user.units.count end assert_no_selector :element, :a, 'disabled': 'disabled', - exact_text: Regexp.union(ADD_UNIT_LABELS) - assert_selector '.flash.notice', text: t('units.create.success', unit: @user.units.last.symbol) + exact_text: Regexp.union(LINK_LABELS.fetch_values(:add_unit, :add_subunit)) + assert_selector '.flash.notice', text: t('units.create.success', unit: Unit.all.last.symbol) end # TODO: check proper form/button redisplay and flash messages on add/edit @@ -64,11 +70,7 @@ class UnitsTest < ApplicationSystemTestCase # Once new/edit form is open, attempt to open another one will close it links = {} targets = {} - { - add_unit: t('units.index.add_unit'), - add_subunit: t('units.unit.add_subunit'), - edit: Regexp.union(units.map(&:symbol)) - }.each_pair do |type, labels| + LINK_LABELS.each_pair do |type, labels| links[type] = all(:link_or_button, exact_text: labels).to_a targets[type] = links[type].sample end diff --git a/test/test_helper.rb b/test/test_helper.rb index 2bb8f4f..183e6e8 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -12,24 +12,26 @@ class ActiveSupport::TestCase include ActionMailer::TestHelper include ActionView::Helpers::TranslationHelper - # NOTE: use public #alphanumeric(chars: ...) from Ruby 3.3 onwards - SecureRandom.class_eval do - def self.random_symbol(n = 10) - # Unicode characters: 32-126, 160-383 - choose([*' '..'~', 160.chr(Encoding::UTF_8), *'¡'..'ſ'], n) + # List of categorized Unicode characters: + # * http://www.unicode.org/Public/UNIDATA/UnicodeData.txt + # File format: http://www.unicode.org/L2/L1999/UnicodeData.html + # Select from graphic ranges: L, M, N, P, S, Zs + UNICODE_CHARS = { + 1 => [*"\u0020".."\u007E"], + 2 => [*"\u00A0".."\u00AC", + *"\u00AE".."\u05FF", + *"\u0606".."\u061B", + *"\u061D".."\u06DC", + *"\u06DE".."\u070E", + *"\u0710".."\u07FF"] + } + UNICODE_CHARS.default = UNICODE_CHARS[1] + UNICODE_CHARS[2] + def random_string(bytes = 10) + result = '' + while bytes > 0 + result += UNICODE_CHARS[bytes].sample.tap { |c| bytes -= c.bytesize } end - end - - def randomize_user_password!(user) - random_password.tap { |p| user.update!(password: p) } - end - - def random_password - SecureRandom.alphanumeric rand(Rails.configuration.devise.password_length) - end - - def random_email - "%s@%s.%s" % (1..3).map { SecureRandom.alphanumeric(rand(1..20)) } + result end # Assumes: max >= step and step = 1e[-]N, both as strings @@ -44,6 +46,18 @@ class ActiveSupport::TestCase BigDecimal(step) * d end + def randomize_user_password!(user) + random_password.tap { |p| user.update!(password: p) } + end + + def random_password + Random.alphanumeric rand(Rails.configuration.devise.password_length) + end + + def random_email + "%s@%s.%s" % (1..3).map { Random.alphanumeric(rand(1..20)) } + end + def with_last_email yield(ActionMailer::Base.deliveries.last) end -- 2.49.1