diff --git a/app/services/journals/create_service/association.rb b/app/services/journals/create_service/association.rb index ff0252c2106..bdfa1762b90 100644 --- a/app/services/journals/create_service/association.rb +++ b/app/services/journals/create_service/association.rb @@ -32,19 +32,20 @@ class Journals::CreateService class Association include Helpers - ASSOCIATION_NAMES = %i[ - AgendaItemable - Attachable - CustomComment - Customizable - ProjectPhase - Storable - ].freeze + # Core associations are defined here. Module-specific associations can be defined in engines + # using `Journals::CreateService::Association.register`. + @registry = Set.new(%i[Attachable CustomComment Customizable ProjectPhase]) - def self.for(journable) - ASSOCIATION_NAMES - .map { "Journals::CreateService::#{it}".constantize.new(journable) } - .select(&:associated?) + class << self + def register(*names) + @registry.merge(names.map(&:to_sym)) + end + + def for(journable) + @registry + .map { "Journals::CreateService::#{it}".constantize.new(journable) } + .select(&:associated?) + end end attr_reader :journable diff --git a/modules/meeting/lib/open_project/meeting/engine.rb b/modules/meeting/lib/open_project/meeting/engine.rb index b95779e5d1b..a8df2aa424f 100644 --- a/modules/meeting/lib/open_project/meeting/engine.rb +++ b/modules/meeting/lib/open_project/meeting/engine.rb @@ -195,6 +195,8 @@ module OpenProject::Meeting ::Exports::Register.register do single(::Meeting, Meetings::Exporter) end + + Journals::CreateService::Association.register(:AgendaItemable) end add_api_path :meetings do diff --git a/modules/storages/lib/open_project/storages/engine.rb b/modules/storages/lib/open_project/storages/engine.rb index a6745c65fdd..773da73aa2c 100644 --- a/modules/storages/lib/open_project/storages/engine.rb +++ b/modules/storages/lib/open_project/storages/engine.rb @@ -256,6 +256,8 @@ module OpenProject::Storages # This hook is executed when the module is loaded. config.to_prepare do + Journals::CreateService::Association.register(:Storable) + # Load Storages::Storage descendants due to STI Storages::Storage::InexistentStorage Storages::OneDriveStorage diff --git a/spec/services/journals/create_service/association_spec.rb b/spec/services/journals/create_service/association_spec.rb new file mode 100644 index 00000000000..a6ccd0e9ef1 --- /dev/null +++ b/spec/services/journals/create_service/association_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +RSpec.describe Journals::CreateService::Association do + describe ".register" do + around do |example| + original = described_class.instance_variable_get(:@registry).dup + example.run + ensure + described_class.instance_variable_set(:@registry, original) + end + + it "adds a new name to the registry" do + expect { described_class.register(:NewAssociation) } + .to change { described_class.instance_variable_get(:@registry) } + .to include(:NewAssociation) + end + + it "is idempotent — duplicate registrations are ignored" do + described_class.register(:NewAssociation) + expect { described_class.register(:NewAssociation) } + .not_to change { described_class.instance_variable_get(:@registry).size } + end + + it "accepts strings and coerces them to symbols" do + described_class.register("NewAssociation") + expect(described_class.instance_variable_get(:@registry)).to include(:NewAssociation) + end + end + + describe ".for" do + it "includes core associations for any journable" do + journable = instance_double(WorkPackage, customizable?: true, respond_to?: false) + allow(journable).to receive(:respond_to?).with(:attachable?).and_return(true) + allow(journable).to receive(:respond_to?).with(:custom_comments).and_return(false) + allow(journable).to receive(:respond_to?).with(:file_links).and_return(false) + allow(journable).to receive(:respond_to?).with(:agenda_items).and_return(false) + allow(journable).to receive(:respond_to?).with(:phases).and_return(false) + + associations = described_class.for(journable) + expect(associations.map(&:class)).to include(Journals::CreateService::Attachable) + end + + it "excludes associations whose #associated? returns false" do + journable = instance_double(WorkPackage, customizable?: false, respond_to?: false) + allow(journable).to receive(:respond_to?).and_return(false) + + associations = described_class.for(journable) + expect(associations).to be_empty + end + end +end