##// END OF EJS Templates
Adds methods for loading and adding settings....
Jean-Philippe Lang -
r13337:ca71cf380046
parent child
Show More
@@ -1,234 +1,254
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Setting < ActiveRecord::Base
18 class Setting < ActiveRecord::Base
19
19
20 DATE_FORMATS = [
20 DATE_FORMATS = [
21 '%Y-%m-%d',
21 '%Y-%m-%d',
22 '%d/%m/%Y',
22 '%d/%m/%Y',
23 '%d.%m.%Y',
23 '%d.%m.%Y',
24 '%d-%m-%Y',
24 '%d-%m-%Y',
25 '%m/%d/%Y',
25 '%m/%d/%Y',
26 '%d %b %Y',
26 '%d %b %Y',
27 '%d %B %Y',
27 '%d %B %Y',
28 '%b %d, %Y',
28 '%b %d, %Y',
29 '%B %d, %Y'
29 '%B %d, %Y'
30 ]
30 ]
31
31
32 TIME_FORMATS = [
32 TIME_FORMATS = [
33 '%H:%M',
33 '%H:%M',
34 '%I:%M %p'
34 '%I:%M %p'
35 ]
35 ]
36
36
37 ENCODINGS = %w(US-ASCII
37 ENCODINGS = %w(US-ASCII
38 windows-1250
38 windows-1250
39 windows-1251
39 windows-1251
40 windows-1252
40 windows-1252
41 windows-1253
41 windows-1253
42 windows-1254
42 windows-1254
43 windows-1255
43 windows-1255
44 windows-1256
44 windows-1256
45 windows-1257
45 windows-1257
46 windows-1258
46 windows-1258
47 windows-31j
47 windows-31j
48 ISO-2022-JP
48 ISO-2022-JP
49 ISO-2022-KR
49 ISO-2022-KR
50 ISO-8859-1
50 ISO-8859-1
51 ISO-8859-2
51 ISO-8859-2
52 ISO-8859-3
52 ISO-8859-3
53 ISO-8859-4
53 ISO-8859-4
54 ISO-8859-5
54 ISO-8859-5
55 ISO-8859-6
55 ISO-8859-6
56 ISO-8859-7
56 ISO-8859-7
57 ISO-8859-8
57 ISO-8859-8
58 ISO-8859-9
58 ISO-8859-9
59 ISO-8859-13
59 ISO-8859-13
60 ISO-8859-15
60 ISO-8859-15
61 KOI8-R
61 KOI8-R
62 UTF-8
62 UTF-8
63 UTF-16
63 UTF-16
64 UTF-16BE
64 UTF-16BE
65 UTF-16LE
65 UTF-16LE
66 EUC-JP
66 EUC-JP
67 Shift_JIS
67 Shift_JIS
68 CP932
68 CP932
69 GB18030
69 GB18030
70 GBK
70 GBK
71 ISCII91
71 ISCII91
72 EUC-KR
72 EUC-KR
73 Big5
73 Big5
74 Big5-HKSCS
74 Big5-HKSCS
75 TIS-620)
75 TIS-620)
76
76
77 cattr_accessor :available_settings
77 cattr_accessor :available_settings
78 @@available_settings = YAML::load(File.open("#{Rails.root}/config/settings.yml"))
78 self.available_settings ||= {}
79 Redmine::Plugin.all.each do |plugin|
80 next unless plugin.settings
81 @@available_settings["plugin_#{plugin.id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
82 end
83
79
84 validates_uniqueness_of :name
80 validates_uniqueness_of :name
85 validates_inclusion_of :name, :in => @@available_settings.keys
81 validates_inclusion_of :name, :in => Proc.new {available_settings.keys}
86 validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting|
82 validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting|
87 (s = @@available_settings[setting.name]) && s['format'] == 'int'
83 (s = available_settings[setting.name]) && s['format'] == 'int'
88 }
84 }
89 attr_protected :id
85 attr_protected :id
90
86
91 # Hash used to cache setting values
87 # Hash used to cache setting values
92 @cached_settings = {}
88 @cached_settings = {}
93 @cached_cleared_on = Time.now
89 @cached_cleared_on = Time.now
94
90
95 def value
91 def value
96 v = read_attribute(:value)
92 v = read_attribute(:value)
97 # Unserialize serialized settings
93 # Unserialize serialized settings
98 v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String)
94 v = YAML::load(v) if available_settings[name]['serialized'] && v.is_a?(String)
99 v = v.to_sym if @@available_settings[name]['format'] == 'symbol' && !v.blank?
95 v = v.to_sym if available_settings[name]['format'] == 'symbol' && !v.blank?
100 v
96 v
101 end
97 end
102
98
103 def value=(v)
99 def value=(v)
104 v = v.to_yaml if v && @@available_settings[name] && @@available_settings[name]['serialized']
100 v = v.to_yaml if v && available_settings[name] && available_settings[name]['serialized']
105 write_attribute(:value, v.to_s)
101 write_attribute(:value, v.to_s)
106 end
102 end
107
103
108 # Returns the value of the setting named name
104 # Returns the value of the setting named name
109 def self.[](name)
105 def self.[](name)
110 v = @cached_settings[name]
106 v = @cached_settings[name]
111 v ? v : (@cached_settings[name] = find_or_default(name).value)
107 v ? v : (@cached_settings[name] = find_or_default(name).value)
112 end
108 end
113
109
114 def self.[]=(name, v)
110 def self.[]=(name, v)
115 setting = find_or_default(name)
111 setting = find_or_default(name)
116 setting.value = (v ? v : "")
112 setting.value = (v ? v : "")
117 @cached_settings[name] = nil
113 @cached_settings[name] = nil
118 setting.save
114 setting.save
119 setting.value
115 setting.value
120 end
116 end
121
117
122 # Defines getter and setter for each setting
123 # Then setting values can be read using: Setting.some_setting_name
124 # or set using Setting.some_setting_name = "some value"
125 @@available_settings.each do |name, params|
126 src = <<-END_SRC
127 def self.#{name}
128 self[:#{name}]
129 end
130
131 def self.#{name}?
132 self[:#{name}].to_i > 0
133 end
134
135 def self.#{name}=(value)
136 self[:#{name}] = value
137 end
138 END_SRC
139 class_eval src, __FILE__, __LINE__
140 end
141
142 # Sets a setting value from params
118 # Sets a setting value from params
143 def self.set_from_params(name, params)
119 def self.set_from_params(name, params)
144 params = params.dup
120 params = params.dup
145 params.delete_if {|v| v.blank? } if params.is_a?(Array)
121 params.delete_if {|v| v.blank? } if params.is_a?(Array)
146 params.symbolize_keys! if params.is_a?(Hash)
122 params.symbolize_keys! if params.is_a?(Hash)
147
123
148 m = "#{name}_from_params"
124 m = "#{name}_from_params"
149 if respond_to? m
125 if respond_to? m
150 self[name.to_sym] = send m, params
126 self[name.to_sym] = send m, params
151 else
127 else
152 self[name.to_sym] = params
128 self[name.to_sym] = params
153 end
129 end
154 end
130 end
155
131
156 # Returns a hash suitable for commit_update_keywords setting
132 # Returns a hash suitable for commit_update_keywords setting
157 #
133 #
158 # Example:
134 # Example:
159 # params = {:keywords => ['fixes', 'closes'], :status_id => ["3", "5"], :done_ratio => ["", "100"]}
135 # params = {:keywords => ['fixes', 'closes'], :status_id => ["3", "5"], :done_ratio => ["", "100"]}
160 # Setting.commit_update_keywords_from_params(params)
136 # Setting.commit_update_keywords_from_params(params)
161 # # => [{'keywords => 'fixes', 'status_id' => "3"}, {'keywords => 'closes', 'status_id' => "5", 'done_ratio' => "100"}]
137 # # => [{'keywords => 'fixes', 'status_id' => "3"}, {'keywords => 'closes', 'status_id' => "5", 'done_ratio' => "100"}]
162 def self.commit_update_keywords_from_params(params)
138 def self.commit_update_keywords_from_params(params)
163 s = []
139 s = []
164 if params.is_a?(Hash) && params.key?(:keywords) && params.values.all? {|v| v.is_a? Array}
140 if params.is_a?(Hash) && params.key?(:keywords) && params.values.all? {|v| v.is_a? Array}
165 attributes = params.except(:keywords).keys
141 attributes = params.except(:keywords).keys
166 params[:keywords].each_with_index do |keywords, i|
142 params[:keywords].each_with_index do |keywords, i|
167 next if keywords.blank?
143 next if keywords.blank?
168 s << attributes.inject({}) {|h, a|
144 s << attributes.inject({}) {|h, a|
169 value = params[a][i].to_s
145 value = params[a][i].to_s
170 h[a.to_s] = value if value.present?
146 h[a.to_s] = value if value.present?
171 h
147 h
172 }.merge('keywords' => keywords)
148 }.merge('keywords' => keywords)
173 end
149 end
174 end
150 end
175 s
151 s
176 end
152 end
177
153
178 # Helper that returns an array based on per_page_options setting
154 # Helper that returns an array based on per_page_options setting
179 def self.per_page_options_array
155 def self.per_page_options_array
180 per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
156 per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
181 end
157 end
182
158
183 # Helper that returns a Hash with single update keywords as keys
159 # Helper that returns a Hash with single update keywords as keys
184 def self.commit_update_keywords_array
160 def self.commit_update_keywords_array
185 a = []
161 a = []
186 if commit_update_keywords.is_a?(Array)
162 if commit_update_keywords.is_a?(Array)
187 commit_update_keywords.each do |rule|
163 commit_update_keywords.each do |rule|
188 next unless rule.is_a?(Hash)
164 next unless rule.is_a?(Hash)
189 rule = rule.dup
165 rule = rule.dup
190 rule.delete_if {|k, v| v.blank?}
166 rule.delete_if {|k, v| v.blank?}
191 keywords = rule['keywords'].to_s.downcase.split(",").map(&:strip).reject(&:blank?)
167 keywords = rule['keywords'].to_s.downcase.split(",").map(&:strip).reject(&:blank?)
192 next if keywords.empty?
168 next if keywords.empty?
193 a << rule.merge('keywords' => keywords)
169 a << rule.merge('keywords' => keywords)
194 end
170 end
195 end
171 end
196 a
172 a
197 end
173 end
198
174
199 def self.openid?
175 def self.openid?
200 Object.const_defined?(:OpenID) && self[:openid].to_i > 0
176 Object.const_defined?(:OpenID) && self[:openid].to_i > 0
201 end
177 end
202
178
203 # Checks if settings have changed since the values were read
179 # Checks if settings have changed since the values were read
204 # and clears the cache hash if it's the case
180 # and clears the cache hash if it's the case
205 # Called once per request
181 # Called once per request
206 def self.check_cache
182 def self.check_cache
207 settings_updated_on = Setting.maximum(:updated_on)
183 settings_updated_on = Setting.maximum(:updated_on)
208 if settings_updated_on && @cached_cleared_on <= settings_updated_on
184 if settings_updated_on && @cached_cleared_on <= settings_updated_on
209 clear_cache
185 clear_cache
210 end
186 end
211 end
187 end
212
188
213 # Clears the settings cache
189 # Clears the settings cache
214 def self.clear_cache
190 def self.clear_cache
215 @cached_settings.clear
191 @cached_settings.clear
216 @cached_cleared_on = Time.now
192 @cached_cleared_on = Time.now
217 logger.info "Settings cache cleared." if logger
193 logger.info "Settings cache cleared." if logger
218 end
194 end
219
195
196 def self.define_plugin_setting(plugin)
197 if plugin.settings
198 name = "plugin_#{plugin.id}"
199 define_setting name, {'default' => plugin.settings[:default], 'serialized' => true}
200 end
201 end
202
203 # Defines getter and setter for each setting
204 # Then setting values can be read using: Setting.some_setting_name
205 # or set using Setting.some_setting_name = "some value"
206 def self.define_setting(name, options={})
207 available_settings[name.to_s] = options
208
209 src = <<-END_SRC
210 def self.#{name}
211 self[:#{name}]
212 end
213
214 def self.#{name}?
215 self[:#{name}].to_i > 0
216 end
217
218 def self.#{name}=(value)
219 self[:#{name}] = value
220 end
221 END_SRC
222 class_eval src, __FILE__, __LINE__
223 end
224
225 def self.load_available_settings
226 YAML::load(File.open("#{Rails.root}/config/settings.yml")).each do |name, options|
227 define_setting name, options
228 end
229 end
230
231 def self.load_plugin_settings
232 Redmine::Plugin.all.each do |plugin|
233 define_plugin_setting(plugin)
234 end
235 end
236
237 load_available_settings
238 load_plugin_settings
239
220 private
240 private
221 # Returns the Setting instance for the setting named name
241 # Returns the Setting instance for the setting named name
222 # (record found in database or new record with default value)
242 # (record found in database or new record with default value)
223 def self.find_or_default(name)
243 def self.find_or_default(name)
224 name = name.to_s
244 name = name.to_s
225 raise "There's no setting named #{name}" unless @@available_settings.has_key?(name)
245 raise "There's no setting named #{name}" unless available_settings.has_key?(name)
226 setting = where(:name => name).first
246 setting = where(:name => name).first
227 unless setting
247 unless setting
228 setting = new
248 setting = new
229 setting.name = name
249 setting.name = name
230 setting.value = @@available_settings[name]['default']
250 setting.value = available_settings[name]['default']
231 end
251 end
232 setting
252 setting
233 end
253 end
234 end
254 end
@@ -1,492 +1,497
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine #:nodoc:
18 module Redmine #:nodoc:
19
19
20 class PluginNotFound < StandardError; end
20 class PluginNotFound < StandardError; end
21 class PluginRequirementError < StandardError; end
21 class PluginRequirementError < StandardError; end
22
22
23 # Base class for Redmine plugins.
23 # Base class for Redmine plugins.
24 # Plugins are registered using the <tt>register</tt> class method that acts as the public constructor.
24 # Plugins are registered using the <tt>register</tt> class method that acts as the public constructor.
25 #
25 #
26 # Redmine::Plugin.register :example do
26 # Redmine::Plugin.register :example do
27 # name 'Example plugin'
27 # name 'Example plugin'
28 # author 'John Smith'
28 # author 'John Smith'
29 # description 'This is an example plugin for Redmine'
29 # description 'This is an example plugin for Redmine'
30 # version '0.0.1'
30 # version '0.0.1'
31 # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
31 # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
32 # end
32 # end
33 #
33 #
34 # === Plugin attributes
34 # === Plugin attributes
35 #
35 #
36 # +settings+ is an optional attribute that let the plugin be configurable.
36 # +settings+ is an optional attribute that let the plugin be configurable.
37 # It must be a hash with the following keys:
37 # It must be a hash with the following keys:
38 # * <tt>:default</tt>: default value for the plugin settings
38 # * <tt>:default</tt>: default value for the plugin settings
39 # * <tt>:partial</tt>: path of the configuration partial view, relative to the plugin <tt>app/views</tt> directory
39 # * <tt>:partial</tt>: path of the configuration partial view, relative to the plugin <tt>app/views</tt> directory
40 # Example:
40 # Example:
41 # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
41 # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
42 # In this example, the settings partial will be found here in the plugin directory: <tt>app/views/settings/_settings.rhtml</tt>.
42 # In this example, the settings partial will be found here in the plugin directory: <tt>app/views/settings/_settings.rhtml</tt>.
43 #
43 #
44 # When rendered, the plugin settings value is available as the local variable +settings+
44 # When rendered, the plugin settings value is available as the local variable +settings+
45 class Plugin
45 class Plugin
46 cattr_accessor :directory
46 cattr_accessor :directory
47 self.directory = File.join(Rails.root, 'plugins')
47 self.directory = File.join(Rails.root, 'plugins')
48
48
49 cattr_accessor :public_directory
49 cattr_accessor :public_directory
50 self.public_directory = File.join(Rails.root, 'public', 'plugin_assets')
50 self.public_directory = File.join(Rails.root, 'public', 'plugin_assets')
51
51
52 @registered_plugins = {}
52 @registered_plugins = {}
53 @used_partials = {}
53 @used_partials = {}
54
54
55 class << self
55 class << self
56 attr_reader :registered_plugins
56 attr_reader :registered_plugins
57 private :new
57 private :new
58
58
59 def def_field(*names)
59 def def_field(*names)
60 class_eval do
60 class_eval do
61 names.each do |name|
61 names.each do |name|
62 define_method(name) do |*args|
62 define_method(name) do |*args|
63 args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
63 args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
64 end
64 end
65 end
65 end
66 end
66 end
67 end
67 end
68 end
68 end
69 def_field :name, :description, :url, :author, :author_url, :version, :settings, :directory
69 def_field :name, :description, :url, :author, :author_url, :version, :settings, :directory
70 attr_reader :id
70 attr_reader :id
71
71
72 # Plugin constructor
72 # Plugin constructor
73 def self.register(id, &block)
73 def self.register(id, &block)
74 p = new(id)
74 p = new(id)
75 p.instance_eval(&block)
75 p.instance_eval(&block)
76
76
77 # Set a default name if it was not provided during registration
77 # Set a default name if it was not provided during registration
78 p.name(id.to_s.humanize) if p.name.nil?
78 p.name(id.to_s.humanize) if p.name.nil?
79 # Set a default directory if it was not provided during registration
79 # Set a default directory if it was not provided during registration
80 p.directory(File.join(self.directory, id.to_s)) if p.directory.nil?
80 p.directory(File.join(self.directory, id.to_s)) if p.directory.nil?
81
81
82 # Adds plugin locales if any
82 # Adds plugin locales if any
83 # YAML translation files should be found under <plugin>/config/locales/
83 # YAML translation files should be found under <plugin>/config/locales/
84 Rails.application.config.i18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
84 Rails.application.config.i18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
85
85
86 # Prepends the app/views directory of the plugin to the view path
86 # Prepends the app/views directory of the plugin to the view path
87 view_path = File.join(p.directory, 'app', 'views')
87 view_path = File.join(p.directory, 'app', 'views')
88 if File.directory?(view_path)
88 if File.directory?(view_path)
89 ActionController::Base.prepend_view_path(view_path)
89 ActionController::Base.prepend_view_path(view_path)
90 ActionMailer::Base.prepend_view_path(view_path)
90 ActionMailer::Base.prepend_view_path(view_path)
91 end
91 end
92
92
93 # Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
93 # Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
94 Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
94 Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
95 ActiveSupport::Dependencies.autoload_paths += [dir]
95 ActiveSupport::Dependencies.autoload_paths += [dir]
96 end
96 end
97
97
98 # Defines plugin setting if present
99 if p.settings
100 Setting.define_plugin_setting p
101 end
102
98 # Warn for potential settings[:partial] collisions
103 # Warn for potential settings[:partial] collisions
99 if p.configurable?
104 if p.configurable?
100 partial = p.settings[:partial]
105 partial = p.settings[:partial]
101 if @used_partials[partial]
106 if @used_partials[partial]
102 Rails.logger.warn "WARNING: settings partial '#{partial}' is declared in '#{p.id}' plugin but it is already used by plugin '#{@used_partials[partial]}'. Only one settings view will be used. You may want to contact those plugins authors to fix this."
107 Rails.logger.warn "WARNING: settings partial '#{partial}' is declared in '#{p.id}' plugin but it is already used by plugin '#{@used_partials[partial]}'. Only one settings view will be used. You may want to contact those plugins authors to fix this."
103 end
108 end
104 @used_partials[partial] = p.id
109 @used_partials[partial] = p.id
105 end
110 end
106
111
107 registered_plugins[id] = p
112 registered_plugins[id] = p
108 end
113 end
109
114
110 # Returns an array of all registered plugins
115 # Returns an array of all registered plugins
111 def self.all
116 def self.all
112 registered_plugins.values.sort
117 registered_plugins.values.sort
113 end
118 end
114
119
115 # Finds a plugin by its id
120 # Finds a plugin by its id
116 # Returns a PluginNotFound exception if the plugin doesn't exist
121 # Returns a PluginNotFound exception if the plugin doesn't exist
117 def self.find(id)
122 def self.find(id)
118 registered_plugins[id.to_sym] || raise(PluginNotFound)
123 registered_plugins[id.to_sym] || raise(PluginNotFound)
119 end
124 end
120
125
121 # Clears the registered plugins hash
126 # Clears the registered plugins hash
122 # It doesn't unload installed plugins
127 # It doesn't unload installed plugins
123 def self.clear
128 def self.clear
124 @registered_plugins = {}
129 @registered_plugins = {}
125 end
130 end
126
131
127 # Removes a plugin from the registered plugins
132 # Removes a plugin from the registered plugins
128 # It doesn't unload the plugin
133 # It doesn't unload the plugin
129 def self.unregister(id)
134 def self.unregister(id)
130 @registered_plugins.delete(id)
135 @registered_plugins.delete(id)
131 end
136 end
132
137
133 # Checks if a plugin is installed
138 # Checks if a plugin is installed
134 #
139 #
135 # @param [String] id name of the plugin
140 # @param [String] id name of the plugin
136 def self.installed?(id)
141 def self.installed?(id)
137 registered_plugins[id.to_sym].present?
142 registered_plugins[id.to_sym].present?
138 end
143 end
139
144
140 def self.load
145 def self.load
141 Dir.glob(File.join(self.directory, '*')).sort.each do |directory|
146 Dir.glob(File.join(self.directory, '*')).sort.each do |directory|
142 if File.directory?(directory)
147 if File.directory?(directory)
143 lib = File.join(directory, "lib")
148 lib = File.join(directory, "lib")
144 if File.directory?(lib)
149 if File.directory?(lib)
145 $:.unshift lib
150 $:.unshift lib
146 ActiveSupport::Dependencies.autoload_paths += [lib]
151 ActiveSupport::Dependencies.autoload_paths += [lib]
147 end
152 end
148 initializer = File.join(directory, "init.rb")
153 initializer = File.join(directory, "init.rb")
149 if File.file?(initializer)
154 if File.file?(initializer)
150 require initializer
155 require initializer
151 end
156 end
152 end
157 end
153 end
158 end
154 end
159 end
155
160
156 def initialize(id)
161 def initialize(id)
157 @id = id.to_sym
162 @id = id.to_sym
158 end
163 end
159
164
160 def public_directory
165 def public_directory
161 File.join(self.class.public_directory, id.to_s)
166 File.join(self.class.public_directory, id.to_s)
162 end
167 end
163
168
164 def to_param
169 def to_param
165 id
170 id
166 end
171 end
167
172
168 def assets_directory
173 def assets_directory
169 File.join(directory, 'assets')
174 File.join(directory, 'assets')
170 end
175 end
171
176
172 def <=>(plugin)
177 def <=>(plugin)
173 self.id.to_s <=> plugin.id.to_s
178 self.id.to_s <=> plugin.id.to_s
174 end
179 end
175
180
176 # Sets a requirement on Redmine version
181 # Sets a requirement on Redmine version
177 # Raises a PluginRequirementError exception if the requirement is not met
182 # Raises a PluginRequirementError exception if the requirement is not met
178 #
183 #
179 # Examples
184 # Examples
180 # # Requires Redmine 0.7.3 or higher
185 # # Requires Redmine 0.7.3 or higher
181 # requires_redmine :version_or_higher => '0.7.3'
186 # requires_redmine :version_or_higher => '0.7.3'
182 # requires_redmine '0.7.3'
187 # requires_redmine '0.7.3'
183 #
188 #
184 # # Requires Redmine 0.7.x or higher
189 # # Requires Redmine 0.7.x or higher
185 # requires_redmine '0.7'
190 # requires_redmine '0.7'
186 #
191 #
187 # # Requires a specific Redmine version
192 # # Requires a specific Redmine version
188 # requires_redmine :version => '0.7.3' # 0.7.3 only
193 # requires_redmine :version => '0.7.3' # 0.7.3 only
189 # requires_redmine :version => '0.7' # 0.7.x
194 # requires_redmine :version => '0.7' # 0.7.x
190 # requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
195 # requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
191 #
196 #
192 # # Requires a Redmine version within a range
197 # # Requires a Redmine version within a range
193 # requires_redmine :version => '0.7.3'..'0.9.1' # >= 0.7.3 and <= 0.9.1
198 # requires_redmine :version => '0.7.3'..'0.9.1' # >= 0.7.3 and <= 0.9.1
194 # requires_redmine :version => '0.7'..'0.9' # >= 0.7.x and <= 0.9.x
199 # requires_redmine :version => '0.7'..'0.9' # >= 0.7.x and <= 0.9.x
195 def requires_redmine(arg)
200 def requires_redmine(arg)
196 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
201 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
197 arg.assert_valid_keys(:version, :version_or_higher)
202 arg.assert_valid_keys(:version, :version_or_higher)
198
203
199 current = Redmine::VERSION.to_a
204 current = Redmine::VERSION.to_a
200 arg.each do |k, req|
205 arg.each do |k, req|
201 case k
206 case k
202 when :version_or_higher
207 when :version_or_higher
203 raise ArgumentError.new(":version_or_higher accepts a version string only") unless req.is_a?(String)
208 raise ArgumentError.new(":version_or_higher accepts a version string only") unless req.is_a?(String)
204 unless compare_versions(req, current) <= 0
209 unless compare_versions(req, current) <= 0
205 raise PluginRequirementError.new("#{id} plugin requires Redmine #{req} or higher but current is #{current.join('.')}")
210 raise PluginRequirementError.new("#{id} plugin requires Redmine #{req} or higher but current is #{current.join('.')}")
206 end
211 end
207 when :version
212 when :version
208 req = [req] if req.is_a?(String)
213 req = [req] if req.is_a?(String)
209 if req.is_a?(Array)
214 if req.is_a?(Array)
210 unless req.detect {|ver| compare_versions(ver, current) == 0}
215 unless req.detect {|ver| compare_versions(ver, current) == 0}
211 raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{req.join(', ')} but current is #{current.join('.')}")
216 raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{req.join(', ')} but current is #{current.join('.')}")
212 end
217 end
213 elsif req.is_a?(Range)
218 elsif req.is_a?(Range)
214 unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0
219 unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0
215 raise PluginRequirementError.new("#{id} plugin requires a Redmine version between #{req.first} and #{req.last} but current is #{current.join('.')}")
220 raise PluginRequirementError.new("#{id} plugin requires a Redmine version between #{req.first} and #{req.last} but current is #{current.join('.')}")
216 end
221 end
217 else
222 else
218 raise ArgumentError.new(":version option accepts a version string, an array or a range of versions")
223 raise ArgumentError.new(":version option accepts a version string, an array or a range of versions")
219 end
224 end
220 end
225 end
221 end
226 end
222 true
227 true
223 end
228 end
224
229
225 def compare_versions(requirement, current)
230 def compare_versions(requirement, current)
226 requirement = requirement.split('.').collect(&:to_i)
231 requirement = requirement.split('.').collect(&:to_i)
227 requirement <=> current.slice(0, requirement.size)
232 requirement <=> current.slice(0, requirement.size)
228 end
233 end
229 private :compare_versions
234 private :compare_versions
230
235
231 # Sets a requirement on a Redmine plugin version
236 # Sets a requirement on a Redmine plugin version
232 # Raises a PluginRequirementError exception if the requirement is not met
237 # Raises a PluginRequirementError exception if the requirement is not met
233 #
238 #
234 # Examples
239 # Examples
235 # # Requires a plugin named :foo version 0.7.3 or higher
240 # # Requires a plugin named :foo version 0.7.3 or higher
236 # requires_redmine_plugin :foo, :version_or_higher => '0.7.3'
241 # requires_redmine_plugin :foo, :version_or_higher => '0.7.3'
237 # requires_redmine_plugin :foo, '0.7.3'
242 # requires_redmine_plugin :foo, '0.7.3'
238 #
243 #
239 # # Requires a specific version of a Redmine plugin
244 # # Requires a specific version of a Redmine plugin
240 # requires_redmine_plugin :foo, :version => '0.7.3' # 0.7.3 only
245 # requires_redmine_plugin :foo, :version => '0.7.3' # 0.7.3 only
241 # requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
246 # requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
242 def requires_redmine_plugin(plugin_name, arg)
247 def requires_redmine_plugin(plugin_name, arg)
243 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
248 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
244 arg.assert_valid_keys(:version, :version_or_higher)
249 arg.assert_valid_keys(:version, :version_or_higher)
245
250
246 plugin = Plugin.find(plugin_name)
251 plugin = Plugin.find(plugin_name)
247 current = plugin.version.split('.').collect(&:to_i)
252 current = plugin.version.split('.').collect(&:to_i)
248
253
249 arg.each do |k, v|
254 arg.each do |k, v|
250 v = [] << v unless v.is_a?(Array)
255 v = [] << v unless v.is_a?(Array)
251 versions = v.collect {|s| s.split('.').collect(&:to_i)}
256 versions = v.collect {|s| s.split('.').collect(&:to_i)}
252 case k
257 case k
253 when :version_or_higher
258 when :version_or_higher
254 raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
259 raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
255 unless (current <=> versions.first) >= 0
260 unless (current <=> versions.first) >= 0
256 raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin #{v} or higher but current is #{current.join('.')}")
261 raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin #{v} or higher but current is #{current.join('.')}")
257 end
262 end
258 when :version
263 when :version
259 unless versions.include?(current.slice(0,3))
264 unless versions.include?(current.slice(0,3))
260 raise PluginRequirementError.new("#{id} plugin requires one the following versions of #{plugin_name}: #{v.join(', ')} but current is #{current.join('.')}")
265 raise PluginRequirementError.new("#{id} plugin requires one the following versions of #{plugin_name}: #{v.join(', ')} but current is #{current.join('.')}")
261 end
266 end
262 end
267 end
263 end
268 end
264 true
269 true
265 end
270 end
266
271
267 # Adds an item to the given +menu+.
272 # Adds an item to the given +menu+.
268 # The +id+ parameter (equals to the project id) is automatically added to the url.
273 # The +id+ parameter (equals to the project id) is automatically added to the url.
269 # menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
274 # menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
270 #
275 #
271 # +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu
276 # +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu
272 #
277 #
273 def menu(menu, item, url, options={})
278 def menu(menu, item, url, options={})
274 Redmine::MenuManager.map(menu).push(item, url, options)
279 Redmine::MenuManager.map(menu).push(item, url, options)
275 end
280 end
276 alias :add_menu_item :menu
281 alias :add_menu_item :menu
277
282
278 # Removes +item+ from the given +menu+.
283 # Removes +item+ from the given +menu+.
279 def delete_menu_item(menu, item)
284 def delete_menu_item(menu, item)
280 Redmine::MenuManager.map(menu).delete(item)
285 Redmine::MenuManager.map(menu).delete(item)
281 end
286 end
282
287
283 # Defines a permission called +name+ for the given +actions+.
288 # Defines a permission called +name+ for the given +actions+.
284 #
289 #
285 # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array):
290 # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array):
286 # permission :destroy_contacts, { :contacts => :destroy }
291 # permission :destroy_contacts, { :contacts => :destroy }
287 # permission :view_contacts, { :contacts => [:index, :show] }
292 # permission :view_contacts, { :contacts => [:index, :show] }
288 #
293 #
289 # The +options+ argument is a hash that accept the following keys:
294 # The +options+ argument is a hash that accept the following keys:
290 # * :public => the permission is public if set to true (implicitly given to any user)
295 # * :public => the permission is public if set to true (implicitly given to any user)
291 # * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member
296 # * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member
292 # * :read => set it to true so that the permission is still granted on closed projects
297 # * :read => set it to true so that the permission is still granted on closed projects
293 #
298 #
294 # Examples
299 # Examples
295 # # A permission that is implicitly given to any user
300 # # A permission that is implicitly given to any user
296 # # This permission won't appear on the Roles & Permissions setup screen
301 # # This permission won't appear on the Roles & Permissions setup screen
297 # permission :say_hello, { :example => :say_hello }, :public => true, :read => true
302 # permission :say_hello, { :example => :say_hello }, :public => true, :read => true
298 #
303 #
299 # # A permission that can be given to any user
304 # # A permission that can be given to any user
300 # permission :say_hello, { :example => :say_hello }
305 # permission :say_hello, { :example => :say_hello }
301 #
306 #
302 # # A permission that can be given to registered users only
307 # # A permission that can be given to registered users only
303 # permission :say_hello, { :example => :say_hello }, :require => :loggedin
308 # permission :say_hello, { :example => :say_hello }, :require => :loggedin
304 #
309 #
305 # # A permission that can be given to project members only
310 # # A permission that can be given to project members only
306 # permission :say_hello, { :example => :say_hello }, :require => :member
311 # permission :say_hello, { :example => :say_hello }, :require => :member
307 def permission(name, actions, options = {})
312 def permission(name, actions, options = {})
308 if @project_module
313 if @project_module
309 Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
314 Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
310 else
315 else
311 Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
316 Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
312 end
317 end
313 end
318 end
314
319
315 # Defines a project module, that can be enabled/disabled for each project.
320 # Defines a project module, that can be enabled/disabled for each project.
316 # Permissions defined inside +block+ will be bind to the module.
321 # Permissions defined inside +block+ will be bind to the module.
317 #
322 #
318 # project_module :things do
323 # project_module :things do
319 # permission :view_contacts, { :contacts => [:list, :show] }, :public => true
324 # permission :view_contacts, { :contacts => [:list, :show] }, :public => true
320 # permission :destroy_contacts, { :contacts => :destroy }
325 # permission :destroy_contacts, { :contacts => :destroy }
321 # end
326 # end
322 def project_module(name, &block)
327 def project_module(name, &block)
323 @project_module = name
328 @project_module = name
324 self.instance_eval(&block)
329 self.instance_eval(&block)
325 @project_module = nil
330 @project_module = nil
326 end
331 end
327
332
328 # Registers an activity provider.
333 # Registers an activity provider.
329 #
334 #
330 # Options:
335 # Options:
331 # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
336 # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
332 # * <tt>:default</tt> - setting this option to false will make the events not displayed by default
337 # * <tt>:default</tt> - setting this option to false will make the events not displayed by default
333 #
338 #
334 # A model can provide several activity event types.
339 # A model can provide several activity event types.
335 #
340 #
336 # Examples:
341 # Examples:
337 # register :news
342 # register :news
338 # register :scrums, :class_name => 'Meeting'
343 # register :scrums, :class_name => 'Meeting'
339 # register :issues, :class_name => ['Issue', 'Journal']
344 # register :issues, :class_name => ['Issue', 'Journal']
340 #
345 #
341 # Retrieving events:
346 # Retrieving events:
342 # Associated model(s) must implement the find_events class method.
347 # Associated model(s) must implement the find_events class method.
343 # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
348 # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
344 #
349 #
345 # The following call should return all the scrum events visible by current user that occurred in the 5 last days:
350 # The following call should return all the scrum events visible by current user that occurred in the 5 last days:
346 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
351 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
347 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
352 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
348 #
353 #
349 # Note that :view_scrums permission is required to view these events in the activity view.
354 # Note that :view_scrums permission is required to view these events in the activity view.
350 def activity_provider(*args)
355 def activity_provider(*args)
351 Redmine::Activity.register(*args)
356 Redmine::Activity.register(*args)
352 end
357 end
353
358
354 # Registers a wiki formatter.
359 # Registers a wiki formatter.
355 #
360 #
356 # Parameters:
361 # Parameters:
357 # * +name+ - human-readable name
362 # * +name+ - human-readable name
358 # * +formatter+ - formatter class, which should have an instance method +to_html+
363 # * +formatter+ - formatter class, which should have an instance method +to_html+
359 # * +helper+ - helper module, which will be included by wiki pages
364 # * +helper+ - helper module, which will be included by wiki pages
360 def wiki_format_provider(name, formatter, helper)
365 def wiki_format_provider(name, formatter, helper)
361 Redmine::WikiFormatting.register(name, formatter, helper)
366 Redmine::WikiFormatting.register(name, formatter, helper)
362 end
367 end
363
368
364 # Returns +true+ if the plugin can be configured.
369 # Returns +true+ if the plugin can be configured.
365 def configurable?
370 def configurable?
366 settings && settings.is_a?(Hash) && !settings[:partial].blank?
371 settings && settings.is_a?(Hash) && !settings[:partial].blank?
367 end
372 end
368
373
369 def mirror_assets
374 def mirror_assets
370 source = assets_directory
375 source = assets_directory
371 destination = public_directory
376 destination = public_directory
372 return unless File.directory?(source)
377 return unless File.directory?(source)
373
378
374 source_files = Dir[source + "/**/*"]
379 source_files = Dir[source + "/**/*"]
375 source_dirs = source_files.select { |d| File.directory?(d) }
380 source_dirs = source_files.select { |d| File.directory?(d) }
376 source_files -= source_dirs
381 source_files -= source_dirs
377
382
378 unless source_files.empty?
383 unless source_files.empty?
379 base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
384 base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
380 begin
385 begin
381 FileUtils.mkdir_p(base_target_dir)
386 FileUtils.mkdir_p(base_target_dir)
382 rescue Exception => e
387 rescue Exception => e
383 raise "Could not create directory #{base_target_dir}: " + e.message
388 raise "Could not create directory #{base_target_dir}: " + e.message
384 end
389 end
385 end
390 end
386
391
387 source_dirs.each do |dir|
392 source_dirs.each do |dir|
388 # strip down these paths so we have simple, relative paths we can
393 # strip down these paths so we have simple, relative paths we can
389 # add to the destination
394 # add to the destination
390 target_dir = File.join(destination, dir.gsub(source, ''))
395 target_dir = File.join(destination, dir.gsub(source, ''))
391 begin
396 begin
392 FileUtils.mkdir_p(target_dir)
397 FileUtils.mkdir_p(target_dir)
393 rescue Exception => e
398 rescue Exception => e
394 raise "Could not create directory #{target_dir}: " + e.message
399 raise "Could not create directory #{target_dir}: " + e.message
395 end
400 end
396 end
401 end
397
402
398 source_files.each do |file|
403 source_files.each do |file|
399 begin
404 begin
400 target = File.join(destination, file.gsub(source, ''))
405 target = File.join(destination, file.gsub(source, ''))
401 unless File.exist?(target) && FileUtils.identical?(file, target)
406 unless File.exist?(target) && FileUtils.identical?(file, target)
402 FileUtils.cp(file, target)
407 FileUtils.cp(file, target)
403 end
408 end
404 rescue Exception => e
409 rescue Exception => e
405 raise "Could not copy #{file} to #{target}: " + e.message
410 raise "Could not copy #{file} to #{target}: " + e.message
406 end
411 end
407 end
412 end
408 end
413 end
409
414
410 # Mirrors assets from one or all plugins to public/plugin_assets
415 # Mirrors assets from one or all plugins to public/plugin_assets
411 def self.mirror_assets(name=nil)
416 def self.mirror_assets(name=nil)
412 if name.present?
417 if name.present?
413 find(name).mirror_assets
418 find(name).mirror_assets
414 else
419 else
415 all.each do |plugin|
420 all.each do |plugin|
416 plugin.mirror_assets
421 plugin.mirror_assets
417 end
422 end
418 end
423 end
419 end
424 end
420
425
421 # The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
426 # The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
422 def migration_directory
427 def migration_directory
423 File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate')
428 File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate')
424 end
429 end
425
430
426 # Returns the version number of the latest migration for this plugin. Returns
431 # Returns the version number of the latest migration for this plugin. Returns
427 # nil if this plugin has no migrations.
432 # nil if this plugin has no migrations.
428 def latest_migration
433 def latest_migration
429 migrations.last
434 migrations.last
430 end
435 end
431
436
432 # Returns the version numbers of all migrations for this plugin.
437 # Returns the version numbers of all migrations for this plugin.
433 def migrations
438 def migrations
434 migrations = Dir[migration_directory+"/*.rb"]
439 migrations = Dir[migration_directory+"/*.rb"]
435 migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort
440 migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort
436 end
441 end
437
442
438 # Migrate this plugin to the given version
443 # Migrate this plugin to the given version
439 def migrate(version = nil)
444 def migrate(version = nil)
440 puts "Migrating #{id} (#{name})..."
445 puts "Migrating #{id} (#{name})..."
441 Redmine::Plugin::Migrator.migrate_plugin(self, version)
446 Redmine::Plugin::Migrator.migrate_plugin(self, version)
442 end
447 end
443
448
444 # Migrates all plugins or a single plugin to a given version
449 # Migrates all plugins or a single plugin to a given version
445 # Exemples:
450 # Exemples:
446 # Plugin.migrate
451 # Plugin.migrate
447 # Plugin.migrate('sample_plugin')
452 # Plugin.migrate('sample_plugin')
448 # Plugin.migrate('sample_plugin', 1)
453 # Plugin.migrate('sample_plugin', 1)
449 #
454 #
450 def self.migrate(name=nil, version=nil)
455 def self.migrate(name=nil, version=nil)
451 if name.present?
456 if name.present?
452 find(name).migrate(version)
457 find(name).migrate(version)
453 else
458 else
454 all.each do |plugin|
459 all.each do |plugin|
455 plugin.migrate
460 plugin.migrate
456 end
461 end
457 end
462 end
458 end
463 end
459
464
460 class Migrator < ActiveRecord::Migrator
465 class Migrator < ActiveRecord::Migrator
461 # We need to be able to set the 'current' plugin being migrated.
466 # We need to be able to set the 'current' plugin being migrated.
462 cattr_accessor :current_plugin
467 cattr_accessor :current_plugin
463
468
464 class << self
469 class << self
465 # Runs the migrations from a plugin, up (or down) to the version given
470 # Runs the migrations from a plugin, up (or down) to the version given
466 def migrate_plugin(plugin, version)
471 def migrate_plugin(plugin, version)
467 self.current_plugin = plugin
472 self.current_plugin = plugin
468 return if current_version(plugin) == version
473 return if current_version(plugin) == version
469 migrate(plugin.migration_directory, version)
474 migrate(plugin.migration_directory, version)
470 end
475 end
471
476
472 def current_version(plugin=current_plugin)
477 def current_version(plugin=current_plugin)
473 # Delete migrations that don't match .. to_i will work because the number comes first
478 # Delete migrations that don't match .. to_i will work because the number comes first
474 ::ActiveRecord::Base.connection.select_values(
479 ::ActiveRecord::Base.connection.select_values(
475 "SELECT version FROM #{schema_migrations_table_name}"
480 "SELECT version FROM #{schema_migrations_table_name}"
476 ).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0
481 ).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0
477 end
482 end
478 end
483 end
479
484
480 def migrated
485 def migrated
481 sm_table = self.class.schema_migrations_table_name
486 sm_table = self.class.schema_migrations_table_name
482 ::ActiveRecord::Base.connection.select_values(
487 ::ActiveRecord::Base.connection.select_values(
483 "SELECT version FROM #{sm_table}"
488 "SELECT version FROM #{sm_table}"
484 ).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort
489 ).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort
485 end
490 end
486
491
487 def record_version_state_after_migrating(version)
492 def record_version_state_after_migrating(version)
488 super(version.to_s + "-" + current_plugin.id.to_s)
493 super(version.to_s + "-" + current_plugin.id.to_s)
489 end
494 end
490 end
495 end
491 end
496 end
492 end
497 end
@@ -1,190 +1,192
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class SettingsControllerTest < ActionController::TestCase
20 class SettingsControllerTest < ActionController::TestCase
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :users
22 :users
23
23
24 def setup
24 def setup
25 User.current = nil
25 User.current = nil
26 @request.session[:user_id] = 1 # admin
26 @request.session[:user_id] = 1 # admin
27 end
27 end
28
28
29 def test_index
29 def test_index
30 get :index
30 get :index
31 assert_response :success
31 assert_response :success
32 assert_template 'edit'
32 assert_template 'edit'
33 end
33 end
34
34
35 def test_get_edit
35 def test_get_edit
36 get :edit
36 get :edit
37 assert_response :success
37 assert_response :success
38 assert_template 'edit'
38 assert_template 'edit'
39
39
40 assert_select 'input[name=?][value=""]', 'settings[enabled_scm][]'
40 assert_select 'input[name=?][value=""]', 'settings[enabled_scm][]'
41 end
41 end
42
42
43 def test_get_edit_should_preselect_default_issue_list_columns
43 def test_get_edit_should_preselect_default_issue_list_columns
44 with_settings :issue_list_default_columns => %w(tracker subject status updated_on) do
44 with_settings :issue_list_default_columns => %w(tracker subject status updated_on) do
45 get :edit
45 get :edit
46 assert_response :success
46 assert_response :success
47 end
47 end
48
48
49 assert_select 'select[id=selected_columns][name=?]', 'settings[issue_list_default_columns][]' do
49 assert_select 'select[id=selected_columns][name=?]', 'settings[issue_list_default_columns][]' do
50 assert_select 'option', 4
50 assert_select 'option', 4
51 assert_select 'option[value=tracker]', :text => 'Tracker'
51 assert_select 'option[value=tracker]', :text => 'Tracker'
52 assert_select 'option[value=subject]', :text => 'Subject'
52 assert_select 'option[value=subject]', :text => 'Subject'
53 assert_select 'option[value=status]', :text => 'Status'
53 assert_select 'option[value=status]', :text => 'Status'
54 assert_select 'option[value=updated_on]', :text => 'Updated'
54 assert_select 'option[value=updated_on]', :text => 'Updated'
55 end
55 end
56
56
57 assert_select 'select[id=available_columns]' do
57 assert_select 'select[id=available_columns]' do
58 assert_select 'option[value=tracker]', 0
58 assert_select 'option[value=tracker]', 0
59 assert_select 'option[value=priority]', :text => 'Priority'
59 assert_select 'option[value=priority]', :text => 'Priority'
60 end
60 end
61 end
61 end
62
62
63 def test_get_edit_without_trackers_should_succeed
63 def test_get_edit_without_trackers_should_succeed
64 Tracker.delete_all
64 Tracker.delete_all
65
65
66 get :edit
66 get :edit
67 assert_response :success
67 assert_response :success
68 end
68 end
69
69
70 def test_post_edit_notifications
70 def test_post_edit_notifications
71 post :edit, :settings => {:mail_from => 'functional@test.foo',
71 post :edit, :settings => {:mail_from => 'functional@test.foo',
72 :bcc_recipients => '0',
72 :bcc_recipients => '0',
73 :notified_events => %w(issue_added issue_updated news_added),
73 :notified_events => %w(issue_added issue_updated news_added),
74 :emails_footer => 'Test footer'
74 :emails_footer => 'Test footer'
75 }
75 }
76 assert_redirected_to '/settings'
76 assert_redirected_to '/settings'
77 assert_equal 'functional@test.foo', Setting.mail_from
77 assert_equal 'functional@test.foo', Setting.mail_from
78 assert !Setting.bcc_recipients?
78 assert !Setting.bcc_recipients?
79 assert_equal %w(issue_added issue_updated news_added), Setting.notified_events
79 assert_equal %w(issue_added issue_updated news_added), Setting.notified_events
80 assert_equal 'Test footer', Setting.emails_footer
80 assert_equal 'Test footer', Setting.emails_footer
81 Setting.clear_cache
81 Setting.clear_cache
82 end
82 end
83
83
84 def test_edit_commit_update_keywords
84 def test_edit_commit_update_keywords
85 with_settings :commit_update_keywords => [
85 with_settings :commit_update_keywords => [
86 {"keywords" => "fixes, resolves", "status_id" => "3"},
86 {"keywords" => "fixes, resolves", "status_id" => "3"},
87 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
87 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
88 ] do
88 ] do
89 get :edit
89 get :edit
90 end
90 end
91 assert_response :success
91 assert_response :success
92 assert_select 'tr.commit-keywords', 2
92 assert_select 'tr.commit-keywords', 2
93 assert_select 'tr.commit-keywords:nth-child(1)' do
93 assert_select 'tr.commit-keywords:nth-child(1)' do
94 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves'
94 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves'
95 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
95 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
96 assert_select 'option[value="3"][selected=selected]'
96 assert_select 'option[value="3"][selected=selected]'
97 end
97 end
98 end
98 end
99 assert_select 'tr.commit-keywords:nth-child(2)' do
99 assert_select 'tr.commit-keywords:nth-child(2)' do
100 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
100 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
101 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
101 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
102 assert_select 'option[value="5"][selected=selected]', :text => 'Closed'
102 assert_select 'option[value="5"][selected=selected]', :text => 'Closed'
103 end
103 end
104 assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
104 assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
105 assert_select 'option[value="100"][selected=selected]', :text => '100 %'
105 assert_select 'option[value="100"][selected=selected]', :text => '100 %'
106 end
106 end
107 assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do
107 assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do
108 assert_select 'option[value="2"][selected=selected]', :text => 'Feature request'
108 assert_select 'option[value="2"][selected=selected]', :text => 'Feature request'
109 end
109 end
110 end
110 end
111 end
111 end
112
112
113 def test_edit_without_commit_update_keywords_should_show_blank_line
113 def test_edit_without_commit_update_keywords_should_show_blank_line
114 with_settings :commit_update_keywords => [] do
114 with_settings :commit_update_keywords => [] do
115 get :edit
115 get :edit
116 end
116 end
117 assert_response :success
117 assert_response :success
118 assert_select 'tr.commit-keywords', 1 do
118 assert_select 'tr.commit-keywords', 1 do
119 assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]'
119 assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]'
120 end
120 end
121 end
121 end
122
122
123 def test_post_edit_commit_update_keywords
123 def test_post_edit_commit_update_keywords
124 post :edit, :settings => {
124 post :edit, :settings => {
125 :commit_update_keywords => {
125 :commit_update_keywords => {
126 :keywords => ["resolves", "closes"],
126 :keywords => ["resolves", "closes"],
127 :status_id => ["3", "5"],
127 :status_id => ["3", "5"],
128 :done_ratio => ["", "100"],
128 :done_ratio => ["", "100"],
129 :if_tracker_id => ["", "2"]
129 :if_tracker_id => ["", "2"]
130 }
130 }
131 }
131 }
132 assert_redirected_to '/settings'
132 assert_redirected_to '/settings'
133 assert_equal([
133 assert_equal([
134 {"keywords" => "resolves", "status_id" => "3"},
134 {"keywords" => "resolves", "status_id" => "3"},
135 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
135 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
136 ], Setting.commit_update_keywords)
136 ], Setting.commit_update_keywords)
137 end
137 end
138
138
139 def test_get_plugin_settings
139 def test_get_plugin_settings
140 Setting.stubs(:plugin_foo).returns({'sample_setting' => 'Plugin setting value'})
141 ActionController::Base.append_view_path(File.join(Rails.root, "test/fixtures/plugins"))
140 ActionController::Base.append_view_path(File.join(Rails.root, "test/fixtures/plugins"))
142 Redmine::Plugin.register :foo do
141 Redmine::Plugin.register :foo do
143 settings :partial => "foo_plugin/foo_plugin_settings"
142 settings :partial => "foo_plugin/foo_plugin_settings",
143 :default => {'sample_setting' => 'Plugin setting value'}
144 end
144 end
145
145
146 get :plugin, :id => 'foo'
146 get :plugin, :id => 'foo'
147 assert_response :success
147 assert_response :success
148 assert_template 'plugin'
148 assert_template 'plugin'
149 assert_select 'form[action="/settings/plugin/foo"]' do
149 assert_select 'form[action="/settings/plugin/foo"]' do
150 assert_select 'input[name=?][value=?]', 'settings[sample_setting]', 'Plugin setting value'
150 assert_select 'input[name=?][value=?]', 'settings[sample_setting]', 'Plugin setting value'
151 end
151 end
152 ensure
152 ensure
153 Redmine::Plugin.unregister(:foo)
153 Redmine::Plugin.unregister(:foo)
154 end
154 end
155
155
156 def test_get_invalid_plugin_settings
156 def test_get_invalid_plugin_settings
157 get :plugin, :id => 'none'
157 get :plugin, :id => 'none'
158 assert_response 404
158 assert_response 404
159 end
159 end
160
160
161 def test_get_non_configurable_plugin_settings
161 def test_get_non_configurable_plugin_settings
162 Redmine::Plugin.register(:foo) {}
162 Redmine::Plugin.register(:foo) {}
163
163
164 get :plugin, :id => 'foo'
164 get :plugin, :id => 'foo'
165 assert_response 404
165 assert_response 404
166
166
167 ensure
167 ensure
168 Redmine::Plugin.unregister(:foo)
168 Redmine::Plugin.unregister(:foo)
169 end
169 end
170
170
171 def test_post_plugin_settings
171 def test_post_plugin_settings
172 Setting.expects(:plugin_foo=).with({'sample_setting' => 'Value'}).returns(true)
173 Redmine::Plugin.register(:foo) do
172 Redmine::Plugin.register(:foo) do
174 settings :partial => 'not blank' # so that configurable? is true
173 settings :partial => 'not blank', # so that configurable? is true
174 :default => {'sample_setting' => 'Plugin setting value'}
175 end
175 end
176
176
177 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
177 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
178 assert_redirected_to '/settings/plugin/foo'
178 assert_redirected_to '/settings/plugin/foo'
179
180 assert_equal({'sample_setting' => 'Value'}, Setting.plugin_foo)
179 end
181 end
180
182
181 def test_post_non_configurable_plugin_settings
183 def test_post_non_configurable_plugin_settings
182 Redmine::Plugin.register(:foo) {}
184 Redmine::Plugin.register(:foo) {}
183
185
184 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
186 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
185 assert_response 404
187 assert_response 404
186
188
187 ensure
189 ensure
188 Redmine::Plugin.unregister(:foo)
190 Redmine::Plugin.unregister(:foo)
189 end
191 end
190 end
192 end
General Comments 0
You need to be logged in to leave comments. Login now