Introduce AddionalsLoader, refactor plugin loading

This commit is contained in:
Alexander Meindl 2021-11-27 08:01:01 +01:00
parent 37f4504c16
commit ad2d4b7f1f
9 changed files with 444 additions and 124 deletions

View File

@ -8,7 +8,7 @@ module AdditionalsChartjsHelper
end
def select_options_for_chartjs_colorscheme(selected)
data = YAML.safe_load(ERB.new(File.read(File.join(Additionals.plugin_dir, 'config', 'colorschemes.yml'))).result) || {}
data = YAML.safe_load(ERB.new(File.read(File.join(AdditionalsLoader.plugin_dir, 'config', 'colorschemes.yml'))).result) || {}
grouped_options_for_select data, selected
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module AdditionalsWikiPdfHelper
module AdditionalsWikiHelper
include Redmine::Export::PDF
def wiki_page_to_pdf(page, project)

View File

@ -8,7 +8,7 @@ class AdditionalsFontAwesome
class << self
def load_icons(type)
data = YAML.safe_load(ERB.new(File.read(File.join(Additionals.plugin_dir, 'config', 'fontawesome_icons.yml'))).result) || {}
data = YAML.safe_load(ERB.new(File.read(File.join(AdditionalsLoader.plugin_dir, 'config', 'fontawesome_icons.yml'))).result) || {}
icons = {}
data.each do |key, values|
icons[key] = { unicode: values['unicode'], label: values['label'] } if values['styles'].include? convert_type2style(type)

View File

@ -0,0 +1,244 @@
# frozen_string_literal: true
class AdditionalsLoader
class ExistingControllerPatchForHelper < StandardError; end
attr_accessor :plugin_id, :debug
class << self
def default_settings(plugin_id = 'additionals')
cached_settings_name = "@default_settings_#{plugin_id}"
cached_settings = instance_variable_get cached_settings_name
if cached_settings.nil?
data = YAML.safe_load(ERB.new(File.read(File.join(plugin_dir(plugin_id), '/config/settings.yml'))).result) || {}
instance_variable_set cached_settings_name, data.symbolize_keys
else
cached_settings
end
end
def plugin_dir(plugin_id = 'additionals')
if Gem.loaded_specs[plugin_id].nil?
File.join Redmine::Plugin.directory, plugin_id
else
Gem.loaded_specs[plugin_id].full_gem_path
end
end
def to_prepare(*args, &block)
if Rails.version > '6.0'
# TODO: This does not work
# see https://www.redmine.org/issues/36245
ActiveSupport.on_load(:active_record, &block)
else
# ActiveSupport::Reloader.to_prepare(*args, &block)
Rails.configuration.to_prepare(*args, &block)
end
end
def persisting
Additionals.debug 'Loading persisting...'
yield
end
def after_initialize(&block)
Additionals.debug 'After initialize...'
Rails.application.config.after_initialize(&block)
end
# required multiple times because of this bug: https://www.redmine.org/issues/33290
def redmine_database_ready?(with_table = nil)
ActiveRecord::Base.connection
rescue ActiveRecord::NoDatabaseError
false
else
with_table.nil? || ActiveRecord::Base.connection.table_exists?(with_table)
end
end
def initialize(plugin_id: 'additionals', debug: false)
self.plugin_id = plugin_id
self.debug = debug
apply_reset
end
def apply_reset
@patches = []
@helpers = []
@global_helpers = []
end
def plugin_dir
@plugin_dir ||= self.class.plugin_dir plugin_id
end
# use_app: false => :plugin_dir/lib/:plugin_id directory
def require_files(spec, use_app: false, reverse: false)
dir = if use_app
File.join plugin_dir, 'app', spec
else
File.join plugin_dir, 'lib', plugin_id, spec
end
files = Dir[dir].sort
files.reverse! if reverse
files.each { |f| require f }
end
def incompatible?(plugins = [])
plugins.each do |plugin|
raise "\n\033[31m#{plugin_id} plugin cannot be used with #{plugin} plugin.\033[0m" if Redmine::Plugin.installed? plugin
end
end
def load_macros!
require_files File.join('wiki_macros', '**/*_macro.rb')
end
def load_hooks!
target = plugin_id.camelize.constantize
target::Hooks
end
def load_custom_field_format!(reverse: false)
require_files File.join('custom_field_formats', '**/*_format.rb'),
reverse: reverse
end
def add_patch(patch)
if patch.is_a? Array
@patches += patch
else
@patches << patch
end
end
def add_helper(helper)
if helper.is_a? Array
@helpers += helper
else
@helpers << helper
end
end
def add_global_helper(helper)
if helper.is_a? Array
@global_helpers += helper
else
@global_helpers << helper
end
end
def apply!
validate_apply
apply_patches!
apply_helpers!
apply_global_helpers!
# reset patches and helpers
apply_reset
true
end
private
def validate_apply
return if @helpers.none? || @patches.none?
controller_patches = @patches.select do |p|
if p.is_a? String
true unless p == p.chomp('Controller')
else
c = p[:target].to_s
true unless c == c.chomp('Controller')
end
end
@helpers.each do |h|
helper_controller = if h.is_a? String
"#{h}Controller"
else
c = h[:controller]
if c.is_a? String
"#{c}Controller"
else
c.to_s
end
end
if controller_patches.include? helper_controller
raise ExistingControllerPatchForHelper, "Do not add helper to #{helper_controller} if patch exists (#{plugin_id})"
end
end
end
def apply_patches!
patches = @patches.map do |p|
if p.is_a? String
{ target: p.constantize, patch: p }
else
patch = p[:patch] || p[:target].to_s
{ target: p[:target], patch: patch }
end
end
patches.uniq!
Additionals.debug "patches for #{plugin_id}: #{patches.inspect}" if debug
patches.each do |patch|
patch_module = if patch[:patch].is_a? String
patch_dir = Rails.root.join "plugins/#{plugin_id}/lib/#{plugin_id}/patches"
require "#{patch_dir}/#{patch[:patch].underscore}_patch"
"#{plugin_id.camelize}::Patches::#{patch[:patch]}Patch".constantize
else
# if module specified (if not string), use it
patch[:patch]
end
target = patch[:target]
target.include patch_module unless target.included_modules.include? patch_module
end
end
def apply_helpers!
helpers = @helpers.map do |h|
if h.is_a? String
{ controller: "#{h}Controller".constantize, helper: "#{plugin_id.camelize}#{h}Helper".constantize }
else
c = h[:controller].is_a?(String) ? "#{h[:controller]}Controller".constantize : h[:controller]
helper = if h[:helper]
h[:helper]
else
helper_name = if h[:controller].is_a? String
h[:controller]
else
h[:controller].to_s.chomp 'Controller'
end
"#{plugin_id.camelize}#{helper_name}Helper".constantize
end
{ controller: c, helper: helper }
end
end
helpers.uniq!
Additionals.debug "helpers for #{plugin_id}: #{helpers.inspect}" if debug
helpers.each do |h|
target = h[:controller]
target.send :helper, h[:helper]
end
end
def apply_global_helpers!
global_helpers = @global_helpers.uniq
Additionals.debug "global helpers for #{plugin_id}: #{global_helpers.inspect}" if debug
global_helpers.each do |h|
ActionView::Base.include h
end
end
end

25
init.rb
View File

@ -11,7 +11,7 @@ Redmine::Plugin.register :additionals do
url 'https://github.com/alphanodes/additionals'
directory __dir__
default_settings = Additionals.load_settings
default_settings = AdditionalsLoader.default_settings
5.times do |i|
default_settings["custom_menu#{i}_name"] = ''
default_settings["custom_menu#{i}_url"] = ''
@ -51,20 +51,23 @@ Redmine::Plugin.register :additionals do
menu :admin_menu, :additionals, { controller: 'settings', action: 'plugin', id: 'additionals' }, caption: :label_additionals
end
Rails.application.config.after_initialize do
AdditionalsLoader.persisting do
Rails.application.paths['app/overrides'] ||= []
Dir.glob(Rails.root.join('plugins/*/app/overrides')).each do |dir|
Rails.application.paths['app/overrides'] << dir unless Rails.application.paths['app/overrides'].include? dir
end
Redmine::AccessControl.include Additionals::Patches::AccessControlPatch
Redmine::AccessControl.singleton_class.prepend Additionals::Patches::AccessControlClassPatch
end
AdditionalsLoader.after_initialize do
# @TODO: this should be moved to AdditionalsFontAwesome and use an instance of it
FONTAWESOME_ICONS = { fab: AdditionalsFontAwesome.load_icons(:fab), # rubocop: disable Lint/ConstantDefinitionInBlock
far: AdditionalsFontAwesome.load_icons(:far),
fas: AdditionalsFontAwesome.load_icons(:fas) }.freeze
end
Rails.application.paths['app/overrides'] ||= []
Dir.glob(Rails.root.join('plugins/*/app/overrides')).each do |dir|
Rails.application.paths['app/overrides'] << dir unless Rails.application.paths['app/overrides'].include? dir
end
if Rails.version > '6.0'
ActiveSupport.on_load(:active_record) { Additionals.setup }
else
Rails.configuration.to_prepare { Additionals.setup }
AdditionalsLoader.to_prepare do
Additionals.setup
end

View File

@ -11,58 +11,58 @@ module Additionals
def setup
RenderAsync.configuration.jquery = true
incompatible_plugins %w[redmine_editauthor
loader = AdditionalsLoader.new
loader.incompatible? %w[redmine_editauthor
redmine_changeauthor
redmine_auto_watch]
ApplicationController.include Additionals::Patches::ApplicationControllerPatch
AutoCompletesController.include Additionals::Patches::AutoCompletesControllerPatch
Issue.include Additionals::Patches::IssuePatch
IssuePriority.include Additionals::Patches::IssuePriorityPatch
TimeEntry.include Additionals::Patches::TimeEntryPatch
Project.include Additionals::Patches::ProjectPatch
Wiki.include Additionals::Patches::WikiPatch
ProjectsController.include Additionals::Patches::ProjectsControllerPatch
WelcomeController.include Additionals::Patches::WelcomeControllerPatch
ReportsController.include Additionals::Patches::ReportsControllerPatch
Principal.include Additionals::Patches::PrincipalPatch
Query.include Additionals::Patches::QueryPatch
QueryFilter.include Additionals::Patches::QueryFilterPatch
Role.include Additionals::Patches::RolePatch
User.include Additionals::Patches::UserPatch
UserPreference.include Additionals::Patches::UserPreferencePatch
loader.add_patch %w[ApplicationController
AutoCompletesController
Issue
IssuePriority
TimeEntry
Project
Wiki
ProjectsController
WelcomeController
ReportsController
Principal
Query
QueryFilter
Role
User
UserPreference]
IssuesController.send :helper, AdditionalsIssuesHelper
SettingsController.send :helper, AdditionalsSettingsHelper
WikiController.send :helper, AdditionalsWikiPdfHelper
CustomFieldsController.send :helper, AdditionalsCustomFieldsHelper
loader.add_helper %w[Issues
Settings
Wiki
CustomFields]
loader.add_global_helper [Additionals::Helpers,
AdditionalsFontawesomeHelper,
AdditionalsMenuHelper,
AdditionalsSelect2Helper]
Redmine::WikiFormatting.format_names.each do |format|
case format
when 'markdown'
Redmine::WikiFormatting::Markdown::HTML.include Patches::FormatterMarkdownPatch
Redmine::WikiFormatting::Markdown::Helper.include Patches::FormattingHelperPatch
loader.add_patch [{ target: Redmine::WikiFormatting::Markdown::HTML, patch: 'FormatterMarkdown' },
{ target: Redmine::WikiFormatting::Markdown::Helper, patch: 'FormattingHelper' }]
when 'textile'
Redmine::WikiFormatting::Textile::Formatter.include Patches::FormatterTextilePatch
Redmine::WikiFormatting::Textile::Helper.include Patches::FormattingHelperPatch
loader.add_patch [{ target: Redmine::WikiFormatting::Textile::Formatter, patch: 'FormatterTextile' },
{ target: Redmine::WikiFormatting::Textile::Helper, patch: 'FormattingHelper' }]
end
end
# Static class patches
Redmine::AccessControl.include Additionals::Patches::AccessControlPatch
Redmine::AccessControl.singleton_class.prepend Additionals::Patches::AccessControlClassPatch
# Global helpers
ActionView::Base.include Additionals::Helpers
ActionView::Base.include AdditionalsFontawesomeHelper
ActionView::Base.include AdditionalsMenuHelper
ActionView::Base.include AdditionalsSelect2Helper
# Apply patches and helper
loader.apply!
# Macros
load_macros
loader.load_macros!
# Hooks
Additionals::Hooks
loader.load_hooks!
end
# support with default setting as fall back
@ -70,7 +70,7 @@ module Additionals
if settings.key? value
settings[value]
else
load_settings[value]
AdditionalsLoader.default_settings[value]
end
end
@ -78,15 +78,6 @@ module Additionals
true? setting(value)
end
# required multiple times because of this bug: https://www.redmine.org/issues/33290
def redmine_database_ready?(with_table = nil)
ActiveRecord::Base.connection
rescue ActiveRecord::NoDatabaseError
false
else
with_table.nil? || ActiveRecord::Base.connection.table_exists?(with_table)
end
def true?(value)
return false if value.is_a? FalseClass
return true if value.is_a?(TrueClass) || value.to_i == 1 || value.to_s.casecmp('true').zero?
@ -124,62 +115,6 @@ module Additionals
timezone.utc_offset - Time.zone.local_to_utc(time).localtime.utc_offset
end
def incompatible_plugins(plugins = [], title = 'additionals')
plugins.each do |plugin|
raise "\n\033[31m#{title} plugin cannot be used with #{plugin} plugin.\033[0m" if Redmine::Plugin.installed? plugin
end
end
# obsolete, do not use this method (it will be removed in next major release)
def patch(patches = [], plugin_id = 'additionals')
patches.each do |name|
patch_dir = Rails.root.join "plugins/#{plugin_id}/lib/#{plugin_id}/patches"
require "#{patch_dir}/#{name.underscore}_patch"
target = name.constantize
patch = "#{plugin_id.camelize}::Patches::#{name}Patch".constantize
target.include patch unless target.included_modules.include? patch
end
end
def load_macros(plugin_id = 'additionals')
Dir[File.join(plugin_dir(plugin_id),
'lib',
plugin_id,
'wiki_macros',
'**/*_macro.rb')].sort.each { |f| require f }
end
def load_custom_field_format(plugin_id, reverse: false)
files = Dir[File.join(plugin_dir(plugin_id),
'lib',
plugin_id,
'custom_field_formats',
'**/*_format.rb')].sort
files.reverse! if reverse
files.each { |f| require f }
end
def plugin_dir(plugin_id = 'additionals')
if Gem.loaded_specs[plugin_id].nil?
File.join Redmine::Plugin.directory, plugin_id
else
Gem.loaded_specs[plugin_id].full_gem_path
end
end
def load_settings(plugin_id = 'additionals')
cached_settings_name = "@load_settings_#{plugin_id}"
cached_settings = instance_variable_get cached_settings_name
if cached_settings.nil?
data = YAML.safe_load(ERB.new(File.read(File.join(plugin_dir(plugin_id), '/config/settings.yml'))).result) || {}
instance_variable_set cached_settings_name, data.symbolize_keys
else
cached_settings
end
end
def hash_remove_with_default(field, options, default = nil)
value = nil
if options.key? field

View File

@ -11,7 +11,7 @@ module Additionals
end
def disabled_project_modules
@database_ready = (Additionals.redmine_database_ready? Setting.table_name) unless defined? @database_ready
@database_ready = (AdditionalsLoader.redmine_database_ready? Setting.table_name) unless defined? @database_ready
return [] unless @database_ready
mods = Additionals.setting(:disabled_modules).to_a.reject(&:blank?)

View File

@ -0,0 +1,145 @@
# frozen_string_literal: true
require File.expand_path '../../test_helper', __FILE__
class AdditionalsLoaderTest < Additionals::TestCase
def test_add_patch
loader = AdditionalsLoader.new
loader.add_patch 'Issue'
assert loader.apply!
end
def test_add_patch_as_hash
loader = AdditionalsLoader.new
loader.add_patch({ target: Issue, patch: 'Issue' })
assert loader.apply!
end
def test_add_patch_as_hash_without_patch
loader = AdditionalsLoader.new
loader.add_patch({ target: Issue })
assert loader.apply!
end
def test_add_multiple_patches
loader = AdditionalsLoader.new
loader.add_patch %w[Issue User]
assert loader.apply!
end
def test_add_invalid_patch
loader = AdditionalsLoader.new
loader.add_patch 'Issue2'
assert_raises NameError do
loader.apply!
end
end
def test_add_helper
loader = AdditionalsLoader.new
loader.add_helper 'Settings'
assert loader.apply!
end
def test_add_helper_as_hash
loader = AdditionalsLoader.new
loader.add_helper({ controller: SettingsController, helper: SettingsHelper })
assert loader.apply!
end
def test_add_helper_as_hash_as_string
loader = AdditionalsLoader.new
loader.add_helper({ controller: 'Settings', helper: 'Settings' })
assert loader.apply!
end
def test_add_helper_as_hash_controller_only
loader = AdditionalsLoader.new
loader.add_helper({ controller: SettingsController })
assert loader.apply!
end
def test_add_helper_as_hash_controller_only_string
loader = AdditionalsLoader.new
loader.add_helper({ controller: 'Settings' })
assert loader.apply!
end
def test_load_macros
loader = AdditionalsLoader.new
macros = loader.load_macros!
assert macros.count.positive?
assert(macros.detect { |macro| macro.include? 'fa_macro' })
end
def test_load_hooks
loader = AdditionalsLoader.new
hooks = loader.load_hooks!
assert hooks.is_a? Module
end
def test_require_files_for_lib
loader = AdditionalsLoader.new
spec = File.join 'wiki_macros', '**/*_macro.rb'
files = loader.require_files spec
assert files.count.positive?
assert(files.detect { |file| file.include? 'fa_macro' })
end
def test_require_files_for_app
loader = AdditionalsLoader.new
spec = File.join 'helpers', '**/additionals_*.rb'
files = loader.require_files spec, use_app: true
assert files.count.positive?
assert(files.detect { |file| file.include? 'additionals_clipboardjs_helper' })
end
def test_apply_without_data
loader = AdditionalsLoader.new
assert loader.apply!
end
def test_apply
loader = AdditionalsLoader.new
loader.add_helper 'Settings'
loader.add_patch 'Issue'
loader.add_global_helper Additionals::Helpers
assert loader.apply!
end
def test_do_not_allow_helper_if_controller_patch_exists
loader = AdditionalsLoader.new
loader.add_patch 'ProjectsController'
loader.add_helper 'Projects'
assert_raises AdditionalsLoader::ExistingControllerPatchForHelper do
assert loader.apply!
end
end
def test_do_not_allow_helper_if_controller_patch_exists_as_hash
loader = AdditionalsLoader.new
loader.add_patch 'ProjectsController'
loader.add_helper({ controller: ProjectsController, helper: 'Settings' })
assert_raises AdditionalsLoader::ExistingControllerPatchForHelper do
assert loader.apply!
end
end
end

View File

@ -62,13 +62,6 @@ class AdditionalsTest < Additionals::TestCase
assert_not Additionals.setting?(:add_go_to_top)
end
def test_load_macros
macros = Additionals.load_macros
assert macros.count.positive?
assert(macros.detect { |macro| macro.include? 'fa_macro' })
end
def test_split_ids
assert_equal [1, 2, 3], Additionals.split_ids('1, 2 , 3')
assert_equal [3, 2], Additionals.split_ids('3, 2, 2')