From 0f008d648c329f94bc8c5ab62e7fd621e1fc277f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Tue, 28 Jul 2026 16:35:36 +0200 Subject: [PATCH 01/18] wip --- app/controllers/connections_controller.rb | 4 +-- ...ower_distribution_unit_socket_decorator.rb | 2 +- app/models/card.rb | 2 +- app/models/card_type.rb | 2 +- app/models/port.rb | 2 +- app/models/port_type.rb | 4 +-- app/views/port_types/show.html.erb | 2 +- ..._rename_power_to_is_power_for_port_type.rb | 28 +++++++++++++++++++ db/schema.rb | 9 ++++-- spec/models/port_type_spec.rb | 4 +-- test/fixtures/port_types.yml | 2 +- 11 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb diff --git a/app/controllers/connections_controller.rb b/app/controllers/connections_controller.rb index 39611517c..92215e23b 100644 --- a/app/controllers/connections_controller.rb +++ b/app/controllers/connections_controller.rb @@ -28,7 +28,7 @@ def edit # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity @coupled_frames = @frame.bay.frames @possible_destination_servers = [] @all_servers_per_frame = [] - if @from_port.is_power_input? && @from_server.is_not_a_pdu? + if @from_port.is_power? && @from_server.is_not_a_pdu? # @coupled_frames.each { |frame| @possible_destination_servers << [frame.name, frame.pdus.collect { |v| [v.name, v.id] }] } # Frame.order(:name).each { |frame| @all_servers_per_frame << [frame.name, frame.pdus.collect { |v| [v.name, v.id] }] } else @@ -46,7 +46,7 @@ def edit # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity # Destination server @to_server = if @to_port.present? @to_port.server - elsif @from_port.is_power_input? + elsif @from_port.is_power? nil # @frame.pdus.first else @frame.servers.where("position IS NOT NULL AND modele_id IS NOT NULL").order(:position).first diff --git a/app/decorators/power_distribution_unit_socket_decorator.rb b/app/decorators/power_distribution_unit_socket_decorator.rb index 41d1c9a29..74395f3bc 100644 --- a/app/decorators/power_distribution_unit_socket_decorator.rb +++ b/app/decorators/power_distribution_unit_socket_decorator.rb @@ -3,7 +3,7 @@ class PowerDistributionUnitSocketDecorator < ApplicationDecorator class << self def port_type_options_for_select - PortTypeDecorator.options_for_select(PortType.where(power: true)) + PortTypeDecorator.options_for_select(PortType.where(is_power: true)) end end end diff --git a/app/models/card.rb b/app/models/card.rb index c4cfd879c..2bad5a143 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -7,7 +7,7 @@ class Card < ApplicationRecord belongs_to :card_type delegate :port_quantity, to: :card_type, allow_nil: true - delegate :is_power_input?, to: :card_type, allow_nil: true + delegate :is_power?, to: :card_type, allow_nil: true belongs_to :twin_card, class_name: "Card", optional: true belongs_to :server, touch: true diff --git a/app/models/card_type.rb b/app/models/card_type.rb index c5ffc77ff..1b0c62596 100644 --- a/app/models/card_type.rb +++ b/app/models/card_type.rb @@ -4,7 +4,7 @@ class CardType < ApplicationRecord has_changelog belongs_to :port_type, counter_cache: true - delegate :is_power_input?, to: :port_type, allow_nil: true + delegate :is_power?, to: :port_type, allow_nil: true has_many :cards, dependent: :restrict_with_error has_many :servers, through: :cards diff --git a/app/models/port.rb b/app/models/port.rb index 2a96c26d1..fc5f20882 100644 --- a/app/models/port.rb +++ b/app/models/port.rb @@ -16,7 +16,7 @@ class Port < ApplicationRecord # rubocop:disable Metrics/ClassLength delegate :server, to: :card, allow_nil: true delegate :circuit, to: :power_distribution_unit_socket, allow_nil: true - delegate :is_power_input?, to: :card, allow_nil: true + delegate :is_power?, to: :card, allow_nil: true delegate :paired_connection, to: :connection, allow_nil: true delegate :color, to: :cable, prefix: true, allow_nil: true delegate :name, to: :cable, prefix: true, allow_nil: true diff --git a/app/models/port_type.rb b/app/models/port_type.rb index 17a1c2fda..cc68b3d0e 100644 --- a/app/models/port_type.rb +++ b/app/models/port_type.rb @@ -10,7 +10,7 @@ class PortType < ApplicationRecord delegate :to_s, to: :name - def is_power_input? - name == "ALIM" + def is_power? + is_power end end diff --git a/app/views/port_types/show.html.erb b/app/views/port_types/show.html.erb index 423eeda18..51d7717cd 100644 --- a/app/views/port_types/show.html.erb +++ b/app/views/port_types/show.html.erb @@ -31,7 +31,7 @@ <% end %>
- <% icon = @port_type.power ? "check" : "x" %> + <% icon = @port_type.is_power ? "check" : "x" %>
<%= Architecture.human_attribute_name(:power) %>
diff --git a/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb b/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb new file mode 100644 index 000000000..8a98daefa --- /dev/null +++ b/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class PortTypeMigration < ApplicationRecord + self.table_name = :port_types +end + +class RenamePowerToIsPowerForPortType < ActiveRecord::Migration[8.1] + def change + rename_column :port_types, :power, :is_power + + create_enum :port_attachable_type, %w[pdu server] + add_column :port_types, :attachable_to, :enum, enum_type: :port_attachable_type, array: true + + reversible do |dir| + dir.up do + PortTypeMigration.find_each do |p| + p.update!(is_power: p.name == "ALIM", attachable_to: [:server]) + end + end + end + + change_table :port_types, bulk: true do |t| + t.change_null :is_power, false + t.change_null :attachable_to, false + t.change_default :attachable_to, from: nil, to: [] + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 98d14ce4a..f6ec6ce8b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,14 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_07_20_131449) do +ActiveRecord::Schema[8.1].define(version: 2026_07_28_132556) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" + # Custom types defined in this database. + # Note that some types may not work with other database engines. Be careful if changing database. + create_enum "port_attachable_type", ["pdu", "server"] + create_table "active_storage_attachments", force: :cascade do |t| t.bigint "blob_id", null: false t.datetime "created_at", precision: nil, null: false @@ -448,9 +452,10 @@ end create_table "port_types", id: :serial, force: :cascade do |t| + t.enum "attachable_to", default: [], null: false, array: true, enum_type: "port_attachable_type" t.integer "card_types_count", default: 0, null: false + t.boolean "is_power", null: false t.string "name" - t.boolean "power" end create_table "ports", id: :serial, force: :cascade do |t| diff --git a/spec/models/port_type_spec.rb b/spec/models/port_type_spec.rb index 1c58081e8..90a91d0c8 100644 --- a/spec/models/port_type_spec.rb +++ b/spec/models/port_type_spec.rb @@ -12,7 +12,7 @@ it { is_expected.to have_many(:sockets).dependent(:restrict_with_error) } end - describe "#is_power_input?" do - it { expect(port_type.is_power_input?).to be true } + describe "#is_power?" do + it { expect(port_type.is_power?).to be true } end end diff --git a/test/fixtures/port_types.yml b/test/fixtures/port_types.yml index 344674c4c..26766ea76 100644 --- a/test/fixtures/port_types.yml +++ b/test/fixtures/port_types.yml @@ -13,7 +13,7 @@ three: four: id: 4 name: ALIM - power: true + is_power: true five: id: 5 From a94eb53b9cdf3a0c100c9c2c9f264eb1f6fdc0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Tue, 28 Jul 2026 16:57:55 +0200 Subject: [PATCH 02/18] fix migration and fixtures --- ...60728132556_rename_power_to_is_power_for_port_type.rb | 9 ++++----- test/fixtures/port_types.yml | 6 ++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb b/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb index 8a98daefa..9c530cbcf 100644 --- a/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb +++ b/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb @@ -11,11 +11,10 @@ def change create_enum :port_attachable_type, %w[pdu server] add_column :port_types, :attachable_to, :enum, enum_type: :port_attachable_type, array: true - reversible do |dir| - dir.up do - PortTypeMigration.find_each do |p| - p.update!(is_power: p.name == "ALIM", attachable_to: [:server]) - end + up_only do + PortTypeMigration.reset_column_information + PortTypeMigration.find_each do |port_type| + port_type.update!(is_power: port_type.name == "ALIM", attachable_to: ["server"]) end end diff --git a/test/fixtures/port_types.yml b/test/fixtures/port_types.yml index 26766ea76..3d6de2647 100644 --- a/test/fixtures/port_types.yml +++ b/test/fixtures/port_types.yml @@ -1,24 +1,30 @@ one: id: 1 name: FC + attachable_to: ["server"] two: id: 2 name: RJ + attachable_to: ["server"] three: id: 3 name: IPMI + attachable_to: ["server"] four: id: 4 name: ALIM is_power: true + attachable_to: ["server"] five: id: 5 name: Five + attachable_to: ["server"] six: id: 6 name: Six + attachable_to: ["server"] From a3e26eb8b8f27bca607f750a137d37e295bc0a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Tue, 28 Jul 2026 17:17:25 +0200 Subject: [PATCH 03/18] fix remaning tests and migrations --- app/views/port_types/_form.html.erb | 4 ++-- app/views/port_types/index.html.erb | 2 +- .../20260728132556_rename_power_to_is_power_for_port_type.rb | 4 +++- db/schema.rb | 2 +- spec/models/port_type_spec.rb | 2 +- test/fixtures/port_types.yml | 5 +++++ 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/views/port_types/_form.html.erb b/app/views/port_types/_form.html.erb index 71d39f3bf..4bc7745c3 100644 --- a/app/views/port_types/_form.html.erb +++ b/app/views/port_types/_form.html.erb @@ -23,8 +23,8 @@
- <%= f.check_box :power, class: "form-check-input" %> - <%= f.label :power, class: "form-check-label" %> + <%= f.check_box :is_power, class: "form-check-input" %> + <%= f.label :is_power, class: "form-check-label" %>
<% end %> diff --git a/app/views/port_types/index.html.erb b/app/views/port_types/index.html.erb index fc6973658..52446899b 100644 --- a/app/views/port_types/index.html.erb +++ b/app/views/port_types/index.html.erb @@ -30,7 +30,7 @@ <% end %> <% table.with_column(PortType.human_attribute_name(:power), sort_by: :power) do |port_type| %> - <%= t("boolean.#{port_type.power?}") %> + <%= t("boolean.#{port_type.is_power?}") %> <% end %> <% table.with_column(PortType.human_attribute_name(:usage), sort_by: :card_types_count) do |port_type| %> diff --git a/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb b/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb index 9c530cbcf..c60b283f5 100644 --- a/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb +++ b/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb @@ -19,9 +19,11 @@ def change end change_table :port_types, bulk: true do |t| + t.change_default :is_power, from: nil, to: false t.change_null :is_power, false - t.change_null :attachable_to, false + t.change_default :attachable_to, from: nil, to: [] + t.change_null :attachable_to, false end end end diff --git a/db/schema.rb b/db/schema.rb index f6ec6ce8b..a928df811 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -454,7 +454,7 @@ create_table "port_types", id: :serial, force: :cascade do |t| t.enum "attachable_to", default: [], null: false, array: true, enum_type: "port_attachable_type" t.integer "card_types_count", default: 0, null: false - t.boolean "is_power", null: false + t.boolean "is_power", default: false, null: false t.string "name" end diff --git a/spec/models/port_type_spec.rb b/spec/models/port_type_spec.rb index 90a91d0c8..431ef3973 100644 --- a/spec/models/port_type_spec.rb +++ b/spec/models/port_type_spec.rb @@ -13,6 +13,6 @@ end describe "#is_power?" do - it { expect(port_type.is_power?).to be true } + it { expect(port_type.is_power?).to be false } end end diff --git a/test/fixtures/port_types.yml b/test/fixtures/port_types.yml index 3d6de2647..c7dfab890 100644 --- a/test/fixtures/port_types.yml +++ b/test/fixtures/port_types.yml @@ -1,16 +1,19 @@ one: id: 1 name: FC + is_power: false attachable_to: ["server"] two: id: 2 name: RJ + is_power: false attachable_to: ["server"] three: id: 3 name: IPMI + is_power: false attachable_to: ["server"] four: @@ -22,9 +25,11 @@ four: five: id: 5 name: Five + is_power: false attachable_to: ["server"] six: id: 6 name: Six + is_power: false attachable_to: ["server"] From 3f8d62f31983d38d34fb78f666c48efcbdcc2a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Tue, 28 Jul 2026 18:01:58 +0200 Subject: [PATCH 04/18] add array inclusion validation and trads --- app/models/port_type.rb | 2 + app/validators/array_inclusion_validator.rb | 12 ++++ config/locales/activerecord.en.yml | 3 + config/locales/activerecord.fr.yml | 3 + spec/models/port_type_spec.rb | 4 ++ .../matchers/validate_array_inclusion_of.rb | 58 +++++++++++++++++++ 6 files changed, 82 insertions(+) create mode 100644 app/validators/array_inclusion_validator.rb create mode 100644 spec/support/matchers/validate_array_inclusion_of.rb diff --git a/app/models/port_type.rb b/app/models/port_type.rb index cc68b3d0e..6733de84e 100644 --- a/app/models/port_type.rb +++ b/app/models/port_type.rb @@ -6,6 +6,8 @@ class PortType < ApplicationRecord has_many :card_types, dependent: :restrict_with_error has_many :sockets, class_name: "PowerDistributionUnit::Socket", dependent: :restrict_with_error + validates :attachable_to, array_inclusion: { in: %w[server] } + scope :sorted, -> { order(name: :asc) } delegate :to_s, to: :name diff --git a/app/validators/array_inclusion_validator.rb b/app/validators/array_inclusion_validator.rb new file mode 100644 index 000000000..3d3a98558 --- /dev/null +++ b/app/validators/array_inclusion_validator.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class ArrayInclusionValidator < ActiveModel::EachValidator + def validate_each(record, attribute, values) + not_allowed_values = values - options[:in] + + return if not_allowed_values.empty? + + wrong_values = not_allowed_values.join(", ") + record.errors.add(attribute, :array_inclusion, wrong_values:) + end +end diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index 5361c1fe2..bf7ccf2ca 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -307,6 +307,9 @@ en: domain_ids: blank: can't be blank, or "All domains" must be checked + messages: + array_inclusion: "includes unauthorized values : %{wrong_values}" + # Common attributes attributes: bay_id: Bay diff --git a/config/locales/activerecord.fr.yml b/config/locales/activerecord.fr.yml index 6ef08c983..c6568c3b7 100644 --- a/config/locales/activerecord.fr.yml +++ b/config/locales/activerecord.fr.yml @@ -544,6 +544,9 @@ fr: domain_ids: blank: doit être rempli, ou "Tous les domaines" doit être coché + messages: + array_inclusion: "contient des valeurs non autorisées : %{wrong_values}" + # Common attributes attributes: bay_id: Baie diff --git a/spec/models/port_type_spec.rb b/spec/models/port_type_spec.rb index 431ef3973..ff022d7cb 100644 --- a/spec/models/port_type_spec.rb +++ b/spec/models/port_type_spec.rb @@ -12,6 +12,10 @@ it { is_expected.to have_many(:sockets).dependent(:restrict_with_error) } end + describe "validations" do + it { is_expected.to validate_array_inclusion_of(:attachable_to).in(%w[server]) } + end + describe "#is_power?" do it { expect(port_type.is_power?).to be false } end diff --git a/spec/support/matchers/validate_array_inclusion_of.rb b/spec/support/matchers/validate_array_inclusion_of.rb new file mode 100644 index 000000000..22367744d --- /dev/null +++ b/spec/support/matchers/validate_array_inclusion_of.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module ModelMatchers + class ValidateArrayInclusionOf + def initialize(attribute) + @attribute = attribute + end + + def in(values) + @values = values + self + end + + def matches?(record) + raise ArgumentError, "Call `.in(values)` before using this matcher" unless @values + + @record = record + + @record.public_send("#{@attribute}=", @values) + return false unless @record.valid? + + @record.public_send("#{@attribute}=", @values + [invalid_value]) + return false if @record.valid? + + @record.errors.added?(@attribute, :array_inclusion, wrong_values: invalid_value) + end + + def description + "validate #{@attribute} contains only allowed values" + end + + def failure_message + "expected #{@record.class} to validate that #{@attribute} only includes allowed values" + end + + def failure_message_when_negated + "expected #{@record.class} not to validate that #{@attribute} only includes allowed values" + end + + private + + def invalid_value + @invalid_value ||= begin + value = "__invalid_value__" + value += "_" while @values.include?(value) + value + end + end + end + + def validate_array_inclusion_of(attribute) + ValidateArrayInclusionOf.new(attribute) + end +end + +RSpec.configure do |config| + config.include ModelMatchers, type: :model +end From 22f29c8b6907c410fd68ca764260f492c252ff3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Wed, 29 Jul 2026 11:40:27 +0200 Subject: [PATCH 05/18] small twicks --- app/models/port_type.rb | 2 +- spec/support/matchers/validate_array_inclusion_of.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/port_type.rb b/app/models/port_type.rb index 6733de84e..8ee882b77 100644 --- a/app/models/port_type.rb +++ b/app/models/port_type.rb @@ -6,7 +6,7 @@ class PortType < ApplicationRecord has_many :card_types, dependent: :restrict_with_error has_many :sockets, class_name: "PowerDistributionUnit::Socket", dependent: :restrict_with_error - validates :attachable_to, array_inclusion: { in: %w[server] } + validates :attachable_to, array_inclusion: { in: %w[pdu server] } scope :sorted, -> { order(name: :asc) } diff --git a/spec/support/matchers/validate_array_inclusion_of.rb b/spec/support/matchers/validate_array_inclusion_of.rb index 22367744d..9c54aa391 100644 --- a/spec/support/matchers/validate_array_inclusion_of.rb +++ b/spec/support/matchers/validate_array_inclusion_of.rb @@ -42,7 +42,7 @@ def failure_message_when_negated def invalid_value @invalid_value ||= begin value = "__invalid_value__" - value += "_" while @values.include?(value) + value = "_#{value}_" while @values.include?(value) value end end From f8521796c8e5363b7f14672c55c0f2e212e85073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Wed, 29 Jul 2026 15:24:45 +0200 Subject: [PATCH 06/18] make decorators use new enum array --- app/decorators/card_type_decorator.rb | 4 ++++ ...power_distribution_unit_socket_decorator.rb | 4 ++-- app/models/cable.rb | 2 +- app/models/port_type.rb | 3 +++ app/services/import_equipment_by_csv.rb | 2 +- app/views/card_types/_form.html.erb | 2 +- app/views/card_types/index.html.erb | 18 ++++++++++-------- .../_socket_fields.html.erb | 2 +- spec/decorators/card_type_decorator_spec.rb | 4 ++++ ..._distribution_unit_socket_decorator_spec.rb | 4 ++-- test/fixtures/port_types.yml | 2 +- 11 files changed, 30 insertions(+), 17 deletions(-) diff --git a/app/decorators/card_type_decorator.rb b/app/decorators/card_type_decorator.rb index e607d99af..6391e4004 100644 --- a/app/decorators/card_type_decorator.rb +++ b/app/decorators/card_type_decorator.rb @@ -4,6 +4,10 @@ class CardTypeDecorator < ApplicationDecorator class << self include ActionView::Helpers::FormOptionsHelper + def port_types_options_for_select + PortTypeDecorator.options_for_select(PortType.attachable_to_server) + end + def grouped_by_port_type_options_for_select(selected = nil) grouped_card_types = CardType.includes(:port_type) .sorted diff --git a/app/decorators/power_distribution_unit_socket_decorator.rb b/app/decorators/power_distribution_unit_socket_decorator.rb index 74395f3bc..36271a21f 100644 --- a/app/decorators/power_distribution_unit_socket_decorator.rb +++ b/app/decorators/power_distribution_unit_socket_decorator.rb @@ -2,8 +2,8 @@ class PowerDistributionUnitSocketDecorator < ApplicationDecorator class << self - def port_type_options_for_select - PortTypeDecorator.options_for_select(PortType.where(is_power: true)) + def port_types_options_for_select + PortTypeDecorator.options_for_select(PortType.attachable_to_pdu) end end end diff --git a/app/models/cable.rb b/app/models/cable.rb index 06afe48ef..f7e79a9cd 100644 --- a/app/models/cable.rb +++ b/app/models/cable.rb @@ -29,7 +29,7 @@ class Cable < ApplicationRecord after_update :touch_ports scope :sorted, lambda { - alim_cables = joins(:port_types).where(port_types: PortType.where(name: "ALIM")).uniq + alim_cables = joins(:port_types).merge(PortType.power_ones).distinct other_cables = order(:cards, ports: { position: :asc }).select { |c| alim_cables.exclude?(c) } other_cables + alim_cables diff --git a/app/models/port_type.rb b/app/models/port_type.rb index 8ee882b77..266460520 100644 --- a/app/models/port_type.rb +++ b/app/models/port_type.rb @@ -9,6 +9,9 @@ class PortType < ApplicationRecord validates :attachable_to, array_inclusion: { in: %w[pdu server] } scope :sorted, -> { order(name: :asc) } + scope :power_ones, -> { where(is_power: true) } + scope :attachable_to_server, -> { where("? = ANY(attachable_to)", :server) } + scope :attachable_to_pdu, -> { where("? = ANY(attachable_to)", :pdu) } delegate :to_s, to: :name diff --git a/app/services/import_equipment_by_csv.rb b/app/services/import_equipment_by_csv.rb index 96b263f99..94fd20230 100644 --- a/app/services/import_equipment_by_csv.rb +++ b/app/services/import_equipment_by_csv.rb @@ -126,7 +126,7 @@ def init_slots(data, server) # SLOTS ALIM valeur = "ALIM" nb_ports = data["Alim"].to_s.gsub(valeur, "").to_i - port_type = PortType.find_or_create_by!(name: valeur) + port_type = PortType.find_or_create_by!(name: valeur, is_power: true) card_alim = CardType.find_or_create_by!(name: "#{nb_ports}#{valeur}", port_quantity: nb_ports, port_type: port_type) Card.find_or_create_by!(card_type: card_alim, server: server, composant: composant_slot_alim) end diff --git a/app/views/card_types/_form.html.erb b/app/views/card_types/_form.html.erb index 28113316e..103ef8562 100644 --- a/app/views/card_types/_form.html.erb +++ b/app/views/card_types/_form.html.erb @@ -24,7 +24,7 @@
<%= f.label :port_type, class: "form-label" %> <%= f.select :port_type_id, - PortType.sorted.map { |pt| [pt.name, pt.id] }, + CardTypeDecorator.port_types_options_for_select, { prompt: true }, { class: "form-select", diff --git a/app/views/card_types/index.html.erb b/app/views/card_types/index.html.erb index 8b9b0be9c..060d9d88a 100644 --- a/app/views/card_types/index.html.erb +++ b/app/views/card_types/index.html.erb @@ -24,14 +24,16 @@
- <%= f.collection_select(:port_type_ids, PortType.sorted, :id, :name, - { prompt: true, multiple: true }, - { class: "form-select", - data: { - controller: :select, - select_clear_button_title_value: t("action.clear_all") - } - }) %> + <%= f.select :port_type_ids, + CardTypeDecorator.port_types_options_for_select, + { prompt: true, multiple: true }, + { + class: "form-select", + data: { + controller: :select, + select_clear_button_title_value: t("action.clear_all"), + }, + } %> <%= f.label :port_type_ids %>
diff --git a/app/views/power_distribution_units/_socket_fields.html.erb b/app/views/power_distribution_units/_socket_fields.html.erb index 4c4e98a16..eb98c2fa1 100644 --- a/app/views/power_distribution_units/_socket_fields.html.erb +++ b/app/views/power_distribution_units/_socket_fields.html.erb @@ -4,7 +4,7 @@
<%= f.select :port_type_id, - PowerDistributionUnitSocketDecorator.port_type_options_for_select, + PowerDistributionUnitSocketDecorator.port_types_options_for_select, { prompt: true }, { class: "form-select form-select-sm", diff --git a/spec/decorators/card_type_decorator_spec.rb b/spec/decorators/card_type_decorator_spec.rb index 22742981b..021e1cf5e 100644 --- a/spec/decorators/card_type_decorator_spec.rb +++ b/spec/decorators/card_type_decorator_spec.rb @@ -6,6 +6,10 @@ let(:card_type) { card_types(:one) } let(:decorated_card_type) { card_type.decorated } + describe ".port_types_options_for_select" do + it { expect(described_class.port_types_options_for_select).to contain_exactly(["ALIM", 4], ["FC", 1], ["Five", 5], ["IPMI", 3], ["RJ", 2], ["Six", 6]) } + end + describe ".grouped_by_port_type_options_for_select" do it do expect(described_class.grouped_by_port_type_options_for_select) diff --git a/spec/decorators/power_distribution_unit_socket_decorator_spec.rb b/spec/decorators/power_distribution_unit_socket_decorator_spec.rb index 72744f598..ee99b60c6 100644 --- a/spec/decorators/power_distribution_unit_socket_decorator_spec.rb +++ b/spec/decorators/power_distribution_unit_socket_decorator_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" RSpec.describe PowerDistributionUnitSocketDecorator, type: :decorator do - describe ".port_type_options_for_select" do - it { expect(described_class.port_type_options_for_select).to contain_exactly(["ALIM", 4]) } + describe ".port_types_options_for_select" do + it { expect(described_class.port_types_options_for_select).to contain_exactly(["ALIM", 4]) } end end diff --git a/test/fixtures/port_types.yml b/test/fixtures/port_types.yml index c7dfab890..82adc04a5 100644 --- a/test/fixtures/port_types.yml +++ b/test/fixtures/port_types.yml @@ -20,7 +20,7 @@ four: id: 4 name: ALIM is_power: true - attachable_to: ["server"] + attachable_to: ["server", "pdu"] five: id: 5 From e047364fccebe5b436446dc9682f63933b6df50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Wed, 29 Jul 2026 15:51:10 +0200 Subject: [PATCH 07/18] fix test --- spec/models/port_type_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/port_type_spec.rb b/spec/models/port_type_spec.rb index ff022d7cb..f1a2d859a 100644 --- a/spec/models/port_type_spec.rb +++ b/spec/models/port_type_spec.rb @@ -13,7 +13,7 @@ end describe "validations" do - it { is_expected.to validate_array_inclusion_of(:attachable_to).in(%w[server]) } + it { is_expected.to validate_array_inclusion_of(:attachable_to).in(%w[pdu server]) } end describe "#is_power?" do From b7ab956e2102298311a63f820d81cf94f8ca2fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Wed, 29 Jul 2026 16:10:57 +0200 Subject: [PATCH 08/18] use function instead of var for clarity --- app/views/port_types/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/port_types/show.html.erb b/app/views/port_types/show.html.erb index 51d7717cd..3209b9b6b 100644 --- a/app/views/port_types/show.html.erb +++ b/app/views/port_types/show.html.erb @@ -31,7 +31,7 @@ <% end %>
- <% icon = @port_type.is_power ? "check" : "x" %> + <% icon = @port_type.is_power? ? "check" : "x" %>
<%= Architecture.human_attribute_name(:power) %>
From 2b7bbbb1656b17a335ec4743a6885061ab6aac1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Fri, 31 Jul 2026 17:02:51 +0200 Subject: [PATCH 09/18] apply review changes wip --- app/decorators/card_type_decorator.rb | 2 +- .../power_distribution_unit_socket_decorator.rb | 2 +- app/models/port_type.rb | 9 ++------- ...8132556_rename_power_to_is_power_for_port_type.rb | 10 +++++----- db/schema.rb | 4 ++-- spec/models/port_type_spec.rb | 2 +- test/fixtures/port_types.yml | 12 ++++++------ 7 files changed, 18 insertions(+), 23 deletions(-) diff --git a/app/decorators/card_type_decorator.rb b/app/decorators/card_type_decorator.rb index 6391e4004..e8a9edf56 100644 --- a/app/decorators/card_type_decorator.rb +++ b/app/decorators/card_type_decorator.rb @@ -5,7 +5,7 @@ class << self include ActionView::Helpers::FormOptionsHelper def port_types_options_for_select - PortTypeDecorator.options_for_select(PortType.attachable_to_server) + PortTypeDecorator.options_for_select(PortType.usable_by(:server)) end def grouped_by_port_type_options_for_select(selected = nil) diff --git a/app/decorators/power_distribution_unit_socket_decorator.rb b/app/decorators/power_distribution_unit_socket_decorator.rb index 36271a21f..3a6458b58 100644 --- a/app/decorators/power_distribution_unit_socket_decorator.rb +++ b/app/decorators/power_distribution_unit_socket_decorator.rb @@ -3,7 +3,7 @@ class PowerDistributionUnitSocketDecorator < ApplicationDecorator class << self def port_types_options_for_select - PortTypeDecorator.options_for_select(PortType.attachable_to_pdu) + PortTypeDecorator.options_for_select(PortType.usable_by(:pdu)) end end end diff --git a/app/models/port_type.rb b/app/models/port_type.rb index 266460520..56384913c 100644 --- a/app/models/port_type.rb +++ b/app/models/port_type.rb @@ -6,16 +6,11 @@ class PortType < ApplicationRecord has_many :card_types, dependent: :restrict_with_error has_many :sockets, class_name: "PowerDistributionUnit::Socket", dependent: :restrict_with_error - validates :attachable_to, array_inclusion: { in: %w[pdu server] } + validates :usable_by, array_inclusion: { in: %w[pdu server] } scope :sorted, -> { order(name: :asc) } scope :power_ones, -> { where(is_power: true) } - scope :attachable_to_server, -> { where("? = ANY(attachable_to)", :server) } - scope :attachable_to_pdu, -> { where("? = ANY(attachable_to)", :pdu) } + scope :usable_by, ->(usable_by) { where("? = ANY(usable_by)", usable_by) } delegate :to_s, to: :name - - def is_power? - is_power - end end diff --git a/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb b/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb index c60b283f5..46100a758 100644 --- a/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb +++ b/db/migrate/20260728132556_rename_power_to_is_power_for_port_type.rb @@ -8,13 +8,13 @@ class RenamePowerToIsPowerForPortType < ActiveRecord::Migration[8.1] def change rename_column :port_types, :power, :is_power - create_enum :port_attachable_type, %w[pdu server] - add_column :port_types, :attachable_to, :enum, enum_type: :port_attachable_type, array: true + create_enum :port_types_usable_by, %w[pdu server] + add_column :port_types, :usable_by, :enum, enum_type: :port_types_usable_by, array: true up_only do PortTypeMigration.reset_column_information PortTypeMigration.find_each do |port_type| - port_type.update!(is_power: port_type.name == "ALIM", attachable_to: ["server"]) + port_type.update!(is_power: port_type.name == "ALIM", usable_by: ["server"]) end end @@ -22,8 +22,8 @@ def change t.change_default :is_power, from: nil, to: false t.change_null :is_power, false - t.change_default :attachable_to, from: nil, to: [] - t.change_null :attachable_to, false + t.change_default :usable_by, from: nil, to: [] + t.change_null :usable_by, false end end end diff --git a/db/schema.rb b/db/schema.rb index a928df811..f8c200240 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -16,7 +16,7 @@ # Custom types defined in this database. # Note that some types may not work with other database engines. Be careful if changing database. - create_enum "port_attachable_type", ["pdu", "server"] + create_enum "port_types_usable_by", ["pdu", "server"] create_table "active_storage_attachments", force: :cascade do |t| t.bigint "blob_id", null: false @@ -452,10 +452,10 @@ end create_table "port_types", id: :serial, force: :cascade do |t| - t.enum "attachable_to", default: [], null: false, array: true, enum_type: "port_attachable_type" t.integer "card_types_count", default: 0, null: false t.boolean "is_power", default: false, null: false t.string "name" + t.enum "usable_by", default: [], null: false, array: true, enum_type: "port_types_usable_by" end create_table "ports", id: :serial, force: :cascade do |t| diff --git a/spec/models/port_type_spec.rb b/spec/models/port_type_spec.rb index f1a2d859a..607c29f26 100644 --- a/spec/models/port_type_spec.rb +++ b/spec/models/port_type_spec.rb @@ -13,7 +13,7 @@ end describe "validations" do - it { is_expected.to validate_array_inclusion_of(:attachable_to).in(%w[pdu server]) } + it { is_expected.to validate_array_inclusion_of(:usable_by).in(%w[pdu server]) } end describe "#is_power?" do diff --git a/test/fixtures/port_types.yml b/test/fixtures/port_types.yml index 82adc04a5..fcfc1583b 100644 --- a/test/fixtures/port_types.yml +++ b/test/fixtures/port_types.yml @@ -2,34 +2,34 @@ one: id: 1 name: FC is_power: false - attachable_to: ["server"] + usable_by: ["server"] two: id: 2 name: RJ is_power: false - attachable_to: ["server"] + usable_by: ["server"] three: id: 3 name: IPMI is_power: false - attachable_to: ["server"] + usable_by: ["server"] four: id: 4 name: ALIM is_power: true - attachable_to: ["server", "pdu"] + usable_by: ["server", "pdu"] five: id: 5 name: Five is_power: false - attachable_to: ["server"] + usable_by: ["server"] six: id: 6 name: Six is_power: false - attachable_to: ["server"] + usable_by: ["server"] From d0534fc8ff6315f01cf965d66a5ac9124407ebf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Fri, 31 Jul 2026 17:43:11 +0200 Subject: [PATCH 10/18] wip --- app/controllers/port_types_controller.rb | 2 +- app/decorators/port_type_decorator.rb | 6 ++++++ app/models/port_type.rb | 3 ++- app/views/port_types/_form.html.erb | 10 ++++++++++ app/views/port_types/index.html.erb | 4 ++++ config/locales/activerecord.en.yml | 4 ++++ config/locales/activerecord.fr.yml | 3 +++ 7 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/controllers/port_types_controller.rb b/app/controllers/port_types_controller.rb index 2aaa93866..bad2a582a 100644 --- a/app/controllers/port_types_controller.rb +++ b/app/controllers/port_types_controller.rb @@ -63,6 +63,6 @@ def set_port_type # Never trust parameters from the scary internet, only allow the white list through. def port_type_params - params.expect(port_type: %i[name power]) + params.expect(port_type: %i[name power usable_by]) end end diff --git a/app/decorators/port_type_decorator.rb b/app/decorators/port_type_decorator.rb index 76a69e223..5696d259c 100644 --- a/app/decorators/port_type_decorator.rb +++ b/app/decorators/port_type_decorator.rb @@ -5,6 +5,12 @@ def self.options_for_select(collection = PortType) collection.select(:id, :name).sorted.map { |p| [p.to_s, p.id] } end + def self.usable_by_options_for_select + PortType::USABLE_BY_VALUES.map do |value| + [I18n.t(".activerecord.attributes.port_type.usable_by/#{value}"), value] + end + end + def css_class_name case name when "RJ", "XRJ" diff --git a/app/models/port_type.rb b/app/models/port_type.rb index 56384913c..1819e4bc9 100644 --- a/app/models/port_type.rb +++ b/app/models/port_type.rb @@ -6,7 +6,8 @@ class PortType < ApplicationRecord has_many :card_types, dependent: :restrict_with_error has_many :sockets, class_name: "PowerDistributionUnit::Socket", dependent: :restrict_with_error - validates :usable_by, array_inclusion: { in: %w[pdu server] } + USABLE_BY_VALUES = %w[pdu server].freeze + validates :usable_by, array_inclusion: { in: USABLE_BY_VALUES } scope :sorted, -> { order(name: :asc) } scope :power_ones, -> { where(is_power: true) } diff --git a/app/views/port_types/_form.html.erb b/app/views/port_types/_form.html.erb index 4bc7745c3..acd954486 100644 --- a/app/views/port_types/_form.html.erb +++ b/app/views/port_types/_form.html.erb @@ -27,6 +27,16 @@ <%= f.label :is_power, class: "form-check-label" %>
+ +
+ <%= f.label :usable_by, class: "form-check-label" %> + <%= f.select :usable_by, PortTypeDecorator.usable_by_options_for_select, + { prompt: true, multiple: true }, + { + class: "form-select", + data: { controller: :select, select_clear_button_title_value: t("action.clear_all"), } + } %> +
<% end %> diff --git a/app/views/port_types/index.html.erb b/app/views/port_types/index.html.erb index 52446899b..f9c7dba40 100644 --- a/app/views/port_types/index.html.erb +++ b/app/views/port_types/index.html.erb @@ -40,6 +40,10 @@ <% end %> <% end %> + <% table.with_column(PortType.human_attribute_name(:usable_by)) do |port_type| %> + <%= port_type.usable_by.to_sentence %> + <% end %> + <% table.with_column(style: "min-width: 70px; width: 70px;") do |port_type| %>
<% if allowed_to?(:update?, port_type) %> diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index bf7ccf2ca..a217b471e 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -17,6 +17,10 @@ en: one: Network hub cluster other: Network hub clusters color: Color + port_type: + usable_by: Utilisation + usable_by/server: Server + usable_by/pdu: Pdu composant: one: Component other: Components diff --git a/config/locales/activerecord.fr.yml b/config/locales/activerecord.fr.yml index c6568c3b7..7ca82676e 100644 --- a/config/locales/activerecord.fr.yml +++ b/config/locales/activerecord.fr.yml @@ -253,6 +253,9 @@ fr: name: Nom power: Alimentation usage: Types de carte compatibles + usable_by: Utilisation + usable_by/server: Serveur + usable_by/pdu: Pdu category: name: Nom description: Description From 88d20910829fa77f663a3bfb0753dfb6efd1cbe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Mon, 3 Aug 2026 17:53:10 +0200 Subject: [PATCH 11/18] add enum array concern --- app/controllers/port_types_controller.rb | 2 +- app/models/concerns/.keep | 0 app/models/concerns/enum/array.rb | 43 ++++++++++++++++++++++++ app/models/port_type.rb | 4 ++- 4 files changed, 47 insertions(+), 2 deletions(-) delete mode 100644 app/models/concerns/.keep create mode 100644 app/models/concerns/enum/array.rb diff --git a/app/controllers/port_types_controller.rb b/app/controllers/port_types_controller.rb index bad2a582a..25fab048e 100644 --- a/app/controllers/port_types_controller.rb +++ b/app/controllers/port_types_controller.rb @@ -63,6 +63,6 @@ def set_port_type # Never trust parameters from the scary internet, only allow the white list through. def port_type_params - params.expect(port_type: %i[name power usable_by]) + params.expect(port_type: [:name, :is_power, { usable_by: [] }]) end end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/models/concerns/enum/array.rb b/app/models/concerns/enum/array.rb new file mode 100644 index 000000000..9cd9532d2 --- /dev/null +++ b/app/models/concerns/enum/array.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Enum + module Array + extend ActiveSupport::Concern + + MISSING_VALUE_MESSAGE = "%s is not a valid value for %s" # rubocop:disable Style/FormatStringToken + private_constant :MISSING_VALUE_MESSAGE + + class_methods do + def array_enum(name = nil, mapping = nil) + name = name.to_s + mapping_hash = ActiveSupport::HashWithIndifferentAccess.new(mapping) + + defined_enums[name] = mapping_hash + + define_singleton_method(name.pluralize) do + mapping_hash + end + + define_method(name) do + Array(self[name]).map { |value| mapping_hash.key(value) } + end + + define_method(:"#{name}=") do |values| + self[name] = Array(values).compact_blank.map do |value| + raise_missing_value(name, value) unless mapping_hash.key?(value) + + mapping_hash[value] + end.uniq + end + + validates name, array_inclusion: { in: mapping_hash.keys } + end + + private + + def raise_missing_value(name, value) + raise ArgumentError, "#{value} is not a valid value for #{name}" + end + end + end +end diff --git a/app/models/port_type.rb b/app/models/port_type.rb index 1819e4bc9..e0e9331d3 100644 --- a/app/models/port_type.rb +++ b/app/models/port_type.rb @@ -1,13 +1,15 @@ # frozen_string_literal: true class PortType < ApplicationRecord + include Enum::Array + has_changelog has_many :card_types, dependent: :restrict_with_error has_many :sockets, class_name: "PowerDistributionUnit::Socket", dependent: :restrict_with_error USABLE_BY_VALUES = %w[pdu server].freeze - validates :usable_by, array_inclusion: { in: USABLE_BY_VALUES } + array_enum :usable_by, USABLE_BY_VALUES.index_with(&:to_s) scope :sorted, -> { order(name: :asc) } scope :power_ones, -> { where(is_power: true) } From 81a0006e318ab72fa0d3cdb166645e6211e5e56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Mon, 3 Aug 2026 18:17:22 +0200 Subject: [PATCH 12/18] fix concern and tests --- app/models/concerns/enum/array.rb | 14 ++++++++------ app/models/port_type.rb | 2 +- .../matchers/validate_array_inclusion_of.rb | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/models/concerns/enum/array.rb b/app/models/concerns/enum/array.rb index 9cd9532d2..a897d882b 100644 --- a/app/models/concerns/enum/array.rb +++ b/app/models/concerns/enum/array.rb @@ -8,7 +8,7 @@ module Array private_constant :MISSING_VALUE_MESSAGE class_methods do - def array_enum(name = nil, mapping = nil) + def array_enum(name = nil, mapping = nil, validate: true) name = name.to_s mapping_hash = ActiveSupport::HashWithIndifferentAccess.new(mapping) @@ -18,19 +18,21 @@ def array_enum(name = nil, mapping = nil) mapping_hash end - define_method(name) do - Array(self[name]).map { |value| mapping_hash.key(value) } + unless validate + define_method(name) do + Array(self[name]).map { |value| mapping_hash.key(value) } + end end define_method(:"#{name}=") do |values| self[name] = Array(values).compact_blank.map do |value| - raise_missing_value(name, value) unless mapping_hash.key?(value) + raise_missing_value(name, value) unless validate || mapping_hash.key?(value) - mapping_hash[value] + mapping_hash[value] || value end.uniq end - validates name, array_inclusion: { in: mapping_hash.keys } + validates name, array_inclusion: { in: mapping_hash.keys } if validate end private diff --git a/app/models/port_type.rb b/app/models/port_type.rb index e0e9331d3..f9265a480 100644 --- a/app/models/port_type.rb +++ b/app/models/port_type.rb @@ -9,7 +9,7 @@ class PortType < ApplicationRecord has_many :sockets, class_name: "PowerDistributionUnit::Socket", dependent: :restrict_with_error USABLE_BY_VALUES = %w[pdu server].freeze - array_enum :usable_by, USABLE_BY_VALUES.index_with(&:to_s) + array_enum :usable_by, USABLE_BY_VALUES.index_with(&:to_s), validate: true scope :sorted, -> { order(name: :asc) } scope :power_ones, -> { where(is_power: true) } diff --git a/spec/support/matchers/validate_array_inclusion_of.rb b/spec/support/matchers/validate_array_inclusion_of.rb index 9c54aa391..4691248c6 100644 --- a/spec/support/matchers/validate_array_inclusion_of.rb +++ b/spec/support/matchers/validate_array_inclusion_of.rb @@ -17,10 +17,10 @@ def matches?(record) @record = record @record.public_send("#{@attribute}=", @values) - return false unless @record.valid? + raise "AAAAAAAAAAAA" unless @record.valid? @record.public_send("#{@attribute}=", @values + [invalid_value]) - return false if @record.valid? + raise "BBBBBBBBBBB" if @record.valid? @record.errors.added?(@attribute, :array_inclusion, wrong_values: invalid_value) end From 1f98ad41e57e35739afe63ccf31ab4b08a221f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Tue, 4 Aug 2026 10:47:31 +0200 Subject: [PATCH 13/18] add usable_by to port_type show --- app/views/port_types/show.html.erb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/views/port_types/show.html.erb b/app/views/port_types/show.html.erb index 3209b9b6b..10ab41a7b 100644 --- a/app/views/port_types/show.html.erb +++ b/app/views/port_types/show.html.erb @@ -17,7 +17,7 @@
<% %i[name].each do |attribute_name| %> -
<%= Architecture.human_attribute_name(attribute_name) %>
+
<%= PortType.human_attribute_name(attribute_name) %>
<%= @port_type.public_send(attribute_name) %>
<% end %>
@@ -32,9 +32,14 @@
<% icon = @port_type.is_power? ? "check" : "x" %> -
<%= Architecture.human_attribute_name(:power) %>
+
<%= PortType.human_attribute_name(:power) %>
+ +
+
<%= PortType.human_attribute_name(:usable_by) %>
+
<%= @port_type.usable_by.to_sentence %>
+
<% end %>
From ef7a8ca6c0624d14367dcf369ae82aebcb74db58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Tue, 4 Aug 2026 11:29:35 +0200 Subject: [PATCH 14/18] add port type decorator --- spec/decorators/port_type_decorator_spec.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/decorators/port_type_decorator_spec.rb b/spec/decorators/port_type_decorator_spec.rb index 0dae51208..84c9ea06b 100644 --- a/spec/decorators/port_type_decorator_spec.rb +++ b/spec/decorators/port_type_decorator_spec.rb @@ -6,13 +6,20 @@ let(:port_type) { port_types(:one) } let(:decorated_port_type) { port_type.decorated } - describe ".alim_options_for_select" do + describe ".options_for_select" do it do expect(described_class.options_for_select) .to contain_exactly(["ALIM", 4], ["FC", 1], ["Five", 5], ["IPMI", 3], ["RJ", 2], ["Six", 6]) end end + describe ".usable_by_options_for_select" do + it do + expect(described_class.usable_by_options_for_select) + .to contain_exactly(%w[Pdu pdu], %w[Serveur server]) + end + end + describe "#css_class_name" do subject(:css_class_name) { decorated_port_type.css_class_name } From 01dab3c927605feaa2593e734baec3808b3a25e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Thu, 6 Aug 2026 17:27:39 +0200 Subject: [PATCH 15/18] apply review changes --- app/decorators/card_type_decorator.rb | 2 +- app/decorators/port_type_decorator.rb | 8 ++++--- ...ower_distribution_unit_socket_decorator.rb | 2 +- app/models/concerns/enum/array.rb | 22 ++++++++++++++----- app/models/port_type.rb | 1 - app/validators/array_inclusion_validator.rb | 2 +- app/views/port_types/index.html.erb | 2 +- app/views/port_types/show.html.erb | 2 +- config/locales/activerecord.en.yml | 12 ++++++---- config/locales/activerecord.fr.yml | 7 +++--- spec/decorators/port_type_decorator_spec.rb | 12 ++++++++++ .../matchers/validate_array_inclusion_of.rb | 6 ++--- 12 files changed, 54 insertions(+), 24 deletions(-) diff --git a/app/decorators/card_type_decorator.rb b/app/decorators/card_type_decorator.rb index e8a9edf56..8bec6d7d4 100644 --- a/app/decorators/card_type_decorator.rb +++ b/app/decorators/card_type_decorator.rb @@ -5,7 +5,7 @@ class << self include ActionView::Helpers::FormOptionsHelper def port_types_options_for_select - PortTypeDecorator.options_for_select(PortType.usable_by(:server)) + PortTypeDecorator.options_for_select(PortType.with_usable_by(:server)) end def grouped_by_port_type_options_for_select(selected = nil) diff --git a/app/decorators/port_type_decorator.rb b/app/decorators/port_type_decorator.rb index 5696d259c..a12091c15 100644 --- a/app/decorators/port_type_decorator.rb +++ b/app/decorators/port_type_decorator.rb @@ -6,9 +6,11 @@ def self.options_for_select(collection = PortType) end def self.usable_by_options_for_select - PortType::USABLE_BY_VALUES.map do |value| - [I18n.t(".activerecord.attributes.port_type.usable_by/#{value}"), value] - end + PortType::USABLE_BY_VALUES.map { |value| [PortType.human_attribute_name("usable_by.#{value}"), value] } + end + + def human_usable_by + usable_by.map { |value| PortType.human_attribute_name("usable_by.#{value}") }.to_sentence end def css_class_name diff --git a/app/decorators/power_distribution_unit_socket_decorator.rb b/app/decorators/power_distribution_unit_socket_decorator.rb index 3a6458b58..0a2da76e8 100644 --- a/app/decorators/power_distribution_unit_socket_decorator.rb +++ b/app/decorators/power_distribution_unit_socket_decorator.rb @@ -3,7 +3,7 @@ class PowerDistributionUnitSocketDecorator < ApplicationDecorator class << self def port_types_options_for_select - PortTypeDecorator.options_for_select(PortType.usable_by(:pdu)) + PortTypeDecorator.options_for_select(PortType.with_usable_by(:pdu)) end end end diff --git a/app/models/concerns/enum/array.rb b/app/models/concerns/enum/array.rb index a897d882b..11cf5c069 100644 --- a/app/models/concerns/enum/array.rb +++ b/app/models/concerns/enum/array.rb @@ -8,7 +8,7 @@ module Array private_constant :MISSING_VALUE_MESSAGE class_methods do - def array_enum(name = nil, mapping = nil, validate: true) + def array_enum(name = nil, mapping = nil, validate: false) name = name.to_s mapping_hash = ActiveSupport::HashWithIndifferentAccess.new(mapping) @@ -18,10 +18,22 @@ def array_enum(name = nil, mapping = nil, validate: true) mapping_hash end - unless validate - define_method(name) do - Array(self[name]).map { |value| mapping_hash.key(value) } + define_singleton_method("with_#{name}") do |*values| + db_values = values.map do |value| + raise_missing_value(name, value) unless mapping_hash.key?(value) + + mapping_hash[value] end + + # get sql type to cast properly + # It returns the array's element type + sql_type = columns_hash[name.to_s].sql_type + + where("#{name} @> ?::#{sql_type}[]", "{#{db_values.join(", ")}}") + end + + define_method(name) do + Array(self[name]).map { |value| mapping_hash.key(value) || value } end define_method(:"#{name}=") do |values| @@ -38,7 +50,7 @@ def array_enum(name = nil, mapping = nil, validate: true) private def raise_missing_value(name, value) - raise ArgumentError, "#{value} is not a valid value for #{name}" + raise ArgumentError, format(MISSING_VALUE_MESSAGE, name:, value:) end end end diff --git a/app/models/port_type.rb b/app/models/port_type.rb index f9265a480..d1a5d8e02 100644 --- a/app/models/port_type.rb +++ b/app/models/port_type.rb @@ -13,7 +13,6 @@ class PortType < ApplicationRecord scope :sorted, -> { order(name: :asc) } scope :power_ones, -> { where(is_power: true) } - scope :usable_by, ->(usable_by) { where("? = ANY(usable_by)", usable_by) } delegate :to_s, to: :name end diff --git a/app/validators/array_inclusion_validator.rb b/app/validators/array_inclusion_validator.rb index 3d3a98558..eeeb85c22 100644 --- a/app/validators/array_inclusion_validator.rb +++ b/app/validators/array_inclusion_validator.rb @@ -7,6 +7,6 @@ def validate_each(record, attribute, values) return if not_allowed_values.empty? wrong_values = not_allowed_values.join(", ") - record.errors.add(attribute, :array_inclusion, wrong_values:) + record.errors.add(attribute, :contains_unpermitted_values, wrong_values:) end end diff --git a/app/views/port_types/index.html.erb b/app/views/port_types/index.html.erb index f9c7dba40..4ffcd8fae 100644 --- a/app/views/port_types/index.html.erb +++ b/app/views/port_types/index.html.erb @@ -41,7 +41,7 @@ <% end %> <% table.with_column(PortType.human_attribute_name(:usable_by)) do |port_type| %> - <%= port_type.usable_by.to_sentence %> + <%= port_type.decorated.human_usable_by %> <% end %> <% table.with_column(style: "min-width: 70px; width: 70px;") do |port_type| %> diff --git a/app/views/port_types/show.html.erb b/app/views/port_types/show.html.erb index 10ab41a7b..658f78282 100644 --- a/app/views/port_types/show.html.erb +++ b/app/views/port_types/show.html.erb @@ -38,7 +38,7 @@
<%= PortType.human_attribute_name(:usable_by) %>
-
<%= @port_type.usable_by.to_sentence %>
+
<%= @port_type.decorated.human_usable_by %>
<% end %> diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index a217b471e..176ed5168 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -18,9 +18,13 @@ en: other: Network hub clusters color: Color port_type: - usable_by: Utilisation - usable_by/server: Server - usable_by/pdu: Pdu + name: Name + power: Power + usage: Usage + usable_by: Usable by + port_type/usable_by: + server: Server + pdu: Pdu composant: one: Component other: Components @@ -312,7 +316,7 @@ en: blank: can't be blank, or "All domains" must be checked messages: - array_inclusion: "includes unauthorized values : %{wrong_values}" + contains_unpermitted_values: "includes unauthorized values: %{wrong_values}" # Common attributes attributes: diff --git a/config/locales/activerecord.fr.yml b/config/locales/activerecord.fr.yml index 7ca82676e..45fe53456 100644 --- a/config/locales/activerecord.fr.yml +++ b/config/locales/activerecord.fr.yml @@ -254,8 +254,9 @@ fr: power: Alimentation usage: Types de carte compatibles usable_by: Utilisation - usable_by/server: Serveur - usable_by/pdu: Pdu + port_type/usable_by: + server: Serveur + pdu: Pdu category: name: Nom description: Description @@ -548,7 +549,7 @@ fr: blank: doit être rempli, ou "Tous les domaines" doit être coché messages: - array_inclusion: "contient des valeurs non autorisées : %{wrong_values}" + contains_unpermitted_values: "contient des valeurs non autorisées : %{wrong_values}" # Common attributes attributes: diff --git a/spec/decorators/port_type_decorator_spec.rb b/spec/decorators/port_type_decorator_spec.rb index 84c9ea06b..f9905f484 100644 --- a/spec/decorators/port_type_decorator_spec.rb +++ b/spec/decorators/port_type_decorator_spec.rb @@ -20,6 +20,18 @@ end end + describe "#human_usable_by" do + context "with one" do + it { expect(decorated_port_type.human_usable_by).to eq("Serveur") } + end + + context "with multiple" do + let(:port_type) { port_types(:four) } + + it { expect(decorated_port_type.human_usable_by).to eq("Serveur et Pdu") } + end + end + describe "#css_class_name" do subject(:css_class_name) { decorated_port_type.css_class_name } diff --git a/spec/support/matchers/validate_array_inclusion_of.rb b/spec/support/matchers/validate_array_inclusion_of.rb index 4691248c6..020c8b67b 100644 --- a/spec/support/matchers/validate_array_inclusion_of.rb +++ b/spec/support/matchers/validate_array_inclusion_of.rb @@ -17,12 +17,12 @@ def matches?(record) @record = record @record.public_send("#{@attribute}=", @values) - raise "AAAAAAAAAAAA" unless @record.valid? + return false unless @record.valid? @record.public_send("#{@attribute}=", @values + [invalid_value]) - raise "BBBBBBBBBBB" if @record.valid? + return false if @record.valid? - @record.errors.added?(@attribute, :array_inclusion, wrong_values: invalid_value) + @record.errors.added?(@attribute, :contains_unpermitted_values, wrong_values: invalid_value) end def description From 2343b6efe3301b115f7f5e8f69512d62d798ccc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Thu, 6 Aug 2026 18:09:31 +0200 Subject: [PATCH 16/18] fix brakeman errors --- app/models/concerns/enum/array.rb | 11 +++++- config/brakeman.ignore | 65 +++++++++++++------------------ 2 files changed, 36 insertions(+), 40 deletions(-) diff --git a/app/models/concerns/enum/array.rb b/app/models/concerns/enum/array.rb index 11cf5c069..e82a6d6fd 100644 --- a/app/models/concerns/enum/array.rb +++ b/app/models/concerns/enum/array.rb @@ -27,9 +27,16 @@ def array_enum(name = nil, mapping = nil, validate: false) # get sql type to cast properly # It returns the array's element type - sql_type = columns_hash[name.to_s].sql_type + element_type = columns_hash[name.to_s].sql_type - where("#{name} @> ?::#{sql_type}[]", "{#{db_values.join(", ")}}") + # encode array so it can be understood by PG + encoder = PG::TextEncoder::Array.new + encoded = encoder.encode(db_values) + + where( + "#{name} @> ?::#{element_type}[]", + encoded, + ) end define_method(name) do diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 2d953f681..82f09dc65 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -7,7 +7,7 @@ "check_name": "LinkToHref", "message": "Potentially unsafe model attribute in `link_to` href", "file": "app/views/servers/show.html.erb", - "line": 145, + "line": 149, "link": "https://brakemanscanner.org/docs/warning_types/link_to_href", "code": "link_to(t(\".documentation\"), Server.new(server_params).documentation_url, :target => :_blank, :rel => :noopener)", "render_path": [ @@ -15,7 +15,7 @@ "type": "controller", "class": "ServersController", "method": "create", - "line": 56, + "line": 58, "file": "app/controllers/servers_controller.rb", "rendered": { "name": "servers/show", @@ -34,6 +34,29 @@ ], "note": "" }, + { + "warning_type": "SQL Injection", + "warning_code": 0, + "fingerprint": "36b99e7859715196aa366a9663589fb7067c66d5e23a87e68e8c922f42779b52", + "check_name": "SQL", + "message": "Possible SQL injection", + "file": "app/models/concerns/enum/array.rb", + "line": 37, + "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", + "code": "where(\"#{name.to_s} @> ?::#{columns_hash[name.to_s].sql_type}[]\", PG::TextEncoder::Array.new.encode(values.map do\n unless ActiveSupport::HashWithIndifferentAccess.new(mapping).key?(value) then\n raise_missing_value(name.to_s, value)\nend\nActiveSupport::HashWithIndifferentAccess.new(mapping)[value]\n end))", + "render_path": null, + "location": { + "type": "method", + "class": "Enum::Array", + "method": "array_enum" + }, + "user_input": "name", + "confidence": "Medium", + "cwe_id": [ + 89 + ], + "note": "" + }, { "warning_type": "Cross-Site Scripting", "warning_code": 4, @@ -41,7 +64,7 @@ "check_name": "LinkToHref", "message": "Unsafe parameter value in `link_to` href", "file": "app/components/filter_component.rb", - "line": 52, + "line": 54, "link": "https://brakemanscanner.org/docs/warning_types/link_to_href", "code": "link_to(url_for(request.query_parameters))", "render_path": null, @@ -57,40 +80,6 @@ ], "note": "" }, - { - "warning_type": "Cross-Site Scripting", - "warning_code": 4, - "fingerprint": "cc17201c40c767e98b13ea607f86fb6cc27b4fad3f56433e55652409ac274331", - "check_name": "LinkToHref", - "message": "Potentially unsafe model attribute in `link_to` href", - "file": "app/views/power_distribution_units/show.html.erb", - "line": 126, - "link": "https://brakemanscanner.org/docs/warning_types/link_to_href", - "code": "link_to(t(\".documentation\"), Server.new(pdu_params).documentation_url, :target => :_blank, :rel => :noopener)", - "render_path": [ - { - "type": "controller", - "class": "PowerDistributionUnitsController", - "method": "create", - "line": 41, - "file": "app/controllers/power_distribution_units_controller.rb", - "rendered": { - "name": "power_distribution_units/show", - "file": "app/views/power_distribution_units/show.html.erb" - } - } - ], - "location": { - "type": "template", - "template": "power_distribution_units/show" - }, - "user_input": "Server.new(pdu_params).documentation_url", - "confidence": "Weak", - "cwe_id": [ - 79 - ], - "note": "" - }, { "warning_type": "Remote Code Execution", "warning_code": 24, @@ -115,5 +104,5 @@ "note": "" } ], - "brakeman_version": "7.1.1" + "brakeman_version": "8.0.5" } From ae4ac74b22c878e9eabdbaa59ca78557377cc59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Fri, 7 Aug 2026 10:47:20 +0200 Subject: [PATCH 17/18] correct power distribution unit socket decorator --- app/decorators/power_distribution_unit_socket_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/decorators/power_distribution_unit_socket_decorator.rb b/app/decorators/power_distribution_unit_socket_decorator.rb index 0a2da76e8..d57bb5d7a 100644 --- a/app/decorators/power_distribution_unit_socket_decorator.rb +++ b/app/decorators/power_distribution_unit_socket_decorator.rb @@ -3,7 +3,7 @@ class PowerDistributionUnitSocketDecorator < ApplicationDecorator class << self def port_types_options_for_select - PortTypeDecorator.options_for_select(PortType.with_usable_by(:pdu)) + PortTypeDecorator.options_for_select(PortType.power_ones.with_usable_by(:pdu)) end end end From ccb13f40cd1280f59e7dc2c3155c325712d48e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Filmont?= Date: Thu, 3 Sep 2026 11:05:48 +0200 Subject: [PATCH 18/18] use simple single scope for enum array concern --- app/decorators/card_type_decorator.rb | 2 +- ...ower_distribution_unit_socket_decorator.rb | 2 +- app/models/concerns/enum/array.rb | 22 +------ config/brakeman.ignore | 65 +++++++++++-------- 4 files changed, 43 insertions(+), 48 deletions(-) diff --git a/app/decorators/card_type_decorator.rb b/app/decorators/card_type_decorator.rb index 8bec6d7d4..5a2fe3f49 100644 --- a/app/decorators/card_type_decorator.rb +++ b/app/decorators/card_type_decorator.rb @@ -5,7 +5,7 @@ class << self include ActionView::Helpers::FormOptionsHelper def port_types_options_for_select - PortTypeDecorator.options_for_select(PortType.with_usable_by(:server)) + PortTypeDecorator.options_for_select(PortType.usable_by_server) end def grouped_by_port_type_options_for_select(selected = nil) diff --git a/app/decorators/power_distribution_unit_socket_decorator.rb b/app/decorators/power_distribution_unit_socket_decorator.rb index d57bb5d7a..ad1450916 100644 --- a/app/decorators/power_distribution_unit_socket_decorator.rb +++ b/app/decorators/power_distribution_unit_socket_decorator.rb @@ -3,7 +3,7 @@ class PowerDistributionUnitSocketDecorator < ApplicationDecorator class << self def port_types_options_for_select - PortTypeDecorator.options_for_select(PortType.power_ones.with_usable_by(:pdu)) + PortTypeDecorator.options_for_select(PortType.power_ones.usable_by_pdu) end end end diff --git a/app/models/concerns/enum/array.rb b/app/models/concerns/enum/array.rb index e82a6d6fd..25bb55652 100644 --- a/app/models/concerns/enum/array.rb +++ b/app/models/concerns/enum/array.rb @@ -10,6 +10,7 @@ module Array class_methods do def array_enum(name = nil, mapping = nil, validate: false) name = name.to_s + column = connection.quote_column_name(name) mapping_hash = ActiveSupport::HashWithIndifferentAccess.new(mapping) defined_enums[name] = mapping_hash @@ -18,25 +19,8 @@ def array_enum(name = nil, mapping = nil, validate: false) mapping_hash end - define_singleton_method("with_#{name}") do |*values| - db_values = values.map do |value| - raise_missing_value(name, value) unless mapping_hash.key?(value) - - mapping_hash[value] - end - - # get sql type to cast properly - # It returns the array's element type - element_type = columns_hash[name.to_s].sql_type - - # encode array so it can be understood by PG - encoder = PG::TextEncoder::Array.new - encoded = encoder.encode(db_values) - - where( - "#{name} @> ?::#{element_type}[]", - encoded, - ) + mapping.each_key do |key| + scope :"#{name}_#{key}", -> { where("? = ANY(#{column})", key) } end define_method(name) do diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 82f09dc65..2d953f681 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -7,7 +7,7 @@ "check_name": "LinkToHref", "message": "Potentially unsafe model attribute in `link_to` href", "file": "app/views/servers/show.html.erb", - "line": 149, + "line": 145, "link": "https://brakemanscanner.org/docs/warning_types/link_to_href", "code": "link_to(t(\".documentation\"), Server.new(server_params).documentation_url, :target => :_blank, :rel => :noopener)", "render_path": [ @@ -15,7 +15,7 @@ "type": "controller", "class": "ServersController", "method": "create", - "line": 58, + "line": 56, "file": "app/controllers/servers_controller.rb", "rendered": { "name": "servers/show", @@ -34,29 +34,6 @@ ], "note": "" }, - { - "warning_type": "SQL Injection", - "warning_code": 0, - "fingerprint": "36b99e7859715196aa366a9663589fb7067c66d5e23a87e68e8c922f42779b52", - "check_name": "SQL", - "message": "Possible SQL injection", - "file": "app/models/concerns/enum/array.rb", - "line": 37, - "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", - "code": "where(\"#{name.to_s} @> ?::#{columns_hash[name.to_s].sql_type}[]\", PG::TextEncoder::Array.new.encode(values.map do\n unless ActiveSupport::HashWithIndifferentAccess.new(mapping).key?(value) then\n raise_missing_value(name.to_s, value)\nend\nActiveSupport::HashWithIndifferentAccess.new(mapping)[value]\n end))", - "render_path": null, - "location": { - "type": "method", - "class": "Enum::Array", - "method": "array_enum" - }, - "user_input": "name", - "confidence": "Medium", - "cwe_id": [ - 89 - ], - "note": "" - }, { "warning_type": "Cross-Site Scripting", "warning_code": 4, @@ -64,7 +41,7 @@ "check_name": "LinkToHref", "message": "Unsafe parameter value in `link_to` href", "file": "app/components/filter_component.rb", - "line": 54, + "line": 52, "link": "https://brakemanscanner.org/docs/warning_types/link_to_href", "code": "link_to(url_for(request.query_parameters))", "render_path": null, @@ -80,6 +57,40 @@ ], "note": "" }, + { + "warning_type": "Cross-Site Scripting", + "warning_code": 4, + "fingerprint": "cc17201c40c767e98b13ea607f86fb6cc27b4fad3f56433e55652409ac274331", + "check_name": "LinkToHref", + "message": "Potentially unsafe model attribute in `link_to` href", + "file": "app/views/power_distribution_units/show.html.erb", + "line": 126, + "link": "https://brakemanscanner.org/docs/warning_types/link_to_href", + "code": "link_to(t(\".documentation\"), Server.new(pdu_params).documentation_url, :target => :_blank, :rel => :noopener)", + "render_path": [ + { + "type": "controller", + "class": "PowerDistributionUnitsController", + "method": "create", + "line": 41, + "file": "app/controllers/power_distribution_units_controller.rb", + "rendered": { + "name": "power_distribution_units/show", + "file": "app/views/power_distribution_units/show.html.erb" + } + } + ], + "location": { + "type": "template", + "template": "power_distribution_units/show" + }, + "user_input": "Server.new(pdu_params).documentation_url", + "confidence": "Weak", + "cwe_id": [ + 79 + ], + "note": "" + }, { "warning_type": "Remote Code Execution", "warning_code": 24, @@ -104,5 +115,5 @@ "note": "" } ], - "brakeman_version": "8.0.5" + "brakeman_version": "7.1.1" }