##// END OF EJS Templates
Don't clear plugins in tests (#16258)....
Jean-Philippe Lang -
r12713:abf24a7d9239
parent child
Show More
@@ -1,475 +1,481
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 class << self
53 class << self
54 attr_reader :registered_plugins
54 attr_reader :registered_plugins
55 private :new
55 private :new
56
56
57 def def_field(*names)
57 def def_field(*names)
58 class_eval do
58 class_eval do
59 names.each do |name|
59 names.each do |name|
60 define_method(name) do |*args|
60 define_method(name) do |*args|
61 args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
61 args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
62 end
62 end
63 end
63 end
64 end
64 end
65 end
65 end
66 end
66 end
67 def_field :name, :description, :url, :author, :author_url, :version, :settings, :directory
67 def_field :name, :description, :url, :author, :author_url, :version, :settings, :directory
68 attr_reader :id
68 attr_reader :id
69
69
70 # Plugin constructor
70 # Plugin constructor
71 def self.register(id, &block)
71 def self.register(id, &block)
72 p = new(id)
72 p = new(id)
73 p.instance_eval(&block)
73 p.instance_eval(&block)
74
74
75 # Set a default name if it was not provided during registration
75 # Set a default name if it was not provided during registration
76 p.name(id.to_s.humanize) if p.name.nil?
76 p.name(id.to_s.humanize) if p.name.nil?
77 # Set a default directory if it was not provided during registration
77 # Set a default directory if it was not provided during registration
78 p.directory(File.join(self.directory, id.to_s)) if p.directory.nil?
78 p.directory(File.join(self.directory, id.to_s)) if p.directory.nil?
79
79
80 # Adds plugin locales if any
80 # Adds plugin locales if any
81 # YAML translation files should be found under <plugin>/config/locales/
81 # YAML translation files should be found under <plugin>/config/locales/
82 Rails.application.config.i18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
82 Rails.application.config.i18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
83
83
84 # Prepends the app/views directory of the plugin to the view path
84 # Prepends the app/views directory of the plugin to the view path
85 view_path = File.join(p.directory, 'app', 'views')
85 view_path = File.join(p.directory, 'app', 'views')
86 if File.directory?(view_path)
86 if File.directory?(view_path)
87 ActionController::Base.prepend_view_path(view_path)
87 ActionController::Base.prepend_view_path(view_path)
88 ActionMailer::Base.prepend_view_path(view_path)
88 ActionMailer::Base.prepend_view_path(view_path)
89 end
89 end
90
90
91 # Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
91 # Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
92 Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
92 Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
93 ActiveSupport::Dependencies.autoload_paths += [dir]
93 ActiveSupport::Dependencies.autoload_paths += [dir]
94 end
94 end
95
95
96 registered_plugins[id] = p
96 registered_plugins[id] = p
97 end
97 end
98
98
99 # Returns an array of all registered plugins
99 # Returns an array of all registered plugins
100 def self.all
100 def self.all
101 registered_plugins.values.sort
101 registered_plugins.values.sort
102 end
102 end
103
103
104 # Finds a plugin by its id
104 # Finds a plugin by its id
105 # Returns a PluginNotFound exception if the plugin doesn't exist
105 # Returns a PluginNotFound exception if the plugin doesn't exist
106 def self.find(id)
106 def self.find(id)
107 registered_plugins[id.to_sym] || raise(PluginNotFound)
107 registered_plugins[id.to_sym] || raise(PluginNotFound)
108 end
108 end
109
109
110 # Clears the registered plugins hash
110 # Clears the registered plugins hash
111 # It doesn't unload installed plugins
111 # It doesn't unload installed plugins
112 def self.clear
112 def self.clear
113 @registered_plugins = {}
113 @registered_plugins = {}
114 end
114 end
115
115
116 # Removes a plugin from the registered plugins
117 # It doesn't unload the plugin
118 def self.unregister(id)
119 @registered_plugins.delete(id)
120 end
121
116 # Checks if a plugin is installed
122 # Checks if a plugin is installed
117 #
123 #
118 # @param [String] id name of the plugin
124 # @param [String] id name of the plugin
119 def self.installed?(id)
125 def self.installed?(id)
120 registered_plugins[id.to_sym].present?
126 registered_plugins[id.to_sym].present?
121 end
127 end
122
128
123 def self.load
129 def self.load
124 Dir.glob(File.join(self.directory, '*')).sort.each do |directory|
130 Dir.glob(File.join(self.directory, '*')).sort.each do |directory|
125 if File.directory?(directory)
131 if File.directory?(directory)
126 lib = File.join(directory, "lib")
132 lib = File.join(directory, "lib")
127 if File.directory?(lib)
133 if File.directory?(lib)
128 $:.unshift lib
134 $:.unshift lib
129 ActiveSupport::Dependencies.autoload_paths += [lib]
135 ActiveSupport::Dependencies.autoload_paths += [lib]
130 end
136 end
131 initializer = File.join(directory, "init.rb")
137 initializer = File.join(directory, "init.rb")
132 if File.file?(initializer)
138 if File.file?(initializer)
133 require initializer
139 require initializer
134 end
140 end
135 end
141 end
136 end
142 end
137 end
143 end
138
144
139 def initialize(id)
145 def initialize(id)
140 @id = id.to_sym
146 @id = id.to_sym
141 end
147 end
142
148
143 def public_directory
149 def public_directory
144 File.join(self.class.public_directory, id.to_s)
150 File.join(self.class.public_directory, id.to_s)
145 end
151 end
146
152
147 def to_param
153 def to_param
148 id
154 id
149 end
155 end
150
156
151 def assets_directory
157 def assets_directory
152 File.join(directory, 'assets')
158 File.join(directory, 'assets')
153 end
159 end
154
160
155 def <=>(plugin)
161 def <=>(plugin)
156 self.id.to_s <=> plugin.id.to_s
162 self.id.to_s <=> plugin.id.to_s
157 end
163 end
158
164
159 # Sets a requirement on Redmine version
165 # Sets a requirement on Redmine version
160 # Raises a PluginRequirementError exception if the requirement is not met
166 # Raises a PluginRequirementError exception if the requirement is not met
161 #
167 #
162 # Examples
168 # Examples
163 # # Requires Redmine 0.7.3 or higher
169 # # Requires Redmine 0.7.3 or higher
164 # requires_redmine :version_or_higher => '0.7.3'
170 # requires_redmine :version_or_higher => '0.7.3'
165 # requires_redmine '0.7.3'
171 # requires_redmine '0.7.3'
166 #
172 #
167 # # Requires Redmine 0.7.x or higher
173 # # Requires Redmine 0.7.x or higher
168 # requires_redmine '0.7'
174 # requires_redmine '0.7'
169 #
175 #
170 # # Requires a specific Redmine version
176 # # Requires a specific Redmine version
171 # requires_redmine :version => '0.7.3' # 0.7.3 only
177 # requires_redmine :version => '0.7.3' # 0.7.3 only
172 # requires_redmine :version => '0.7' # 0.7.x
178 # requires_redmine :version => '0.7' # 0.7.x
173 # requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
179 # requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
174 #
180 #
175 # # Requires a Redmine version within a range
181 # # Requires a Redmine version within a range
176 # requires_redmine :version => '0.7.3'..'0.9.1' # >= 0.7.3 and <= 0.9.1
182 # requires_redmine :version => '0.7.3'..'0.9.1' # >= 0.7.3 and <= 0.9.1
177 # requires_redmine :version => '0.7'..'0.9' # >= 0.7.x and <= 0.9.x
183 # requires_redmine :version => '0.7'..'0.9' # >= 0.7.x and <= 0.9.x
178 def requires_redmine(arg)
184 def requires_redmine(arg)
179 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
185 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
180 arg.assert_valid_keys(:version, :version_or_higher)
186 arg.assert_valid_keys(:version, :version_or_higher)
181
187
182 current = Redmine::VERSION.to_a
188 current = Redmine::VERSION.to_a
183 arg.each do |k, req|
189 arg.each do |k, req|
184 case k
190 case k
185 when :version_or_higher
191 when :version_or_higher
186 raise ArgumentError.new(":version_or_higher accepts a version string only") unless req.is_a?(String)
192 raise ArgumentError.new(":version_or_higher accepts a version string only") unless req.is_a?(String)
187 unless compare_versions(req, current) <= 0
193 unless compare_versions(req, current) <= 0
188 raise PluginRequirementError.new("#{id} plugin requires Redmine #{req} or higher but current is #{current.join('.')}")
194 raise PluginRequirementError.new("#{id} plugin requires Redmine #{req} or higher but current is #{current.join('.')}")
189 end
195 end
190 when :version
196 when :version
191 req = [req] if req.is_a?(String)
197 req = [req] if req.is_a?(String)
192 if req.is_a?(Array)
198 if req.is_a?(Array)
193 unless req.detect {|ver| compare_versions(ver, current) == 0}
199 unless req.detect {|ver| compare_versions(ver, current) == 0}
194 raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{req.join(', ')} but current is #{current.join('.')}")
200 raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{req.join(', ')} but current is #{current.join('.')}")
195 end
201 end
196 elsif req.is_a?(Range)
202 elsif req.is_a?(Range)
197 unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0
203 unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0
198 raise PluginRequirementError.new("#{id} plugin requires a Redmine version between #{req.first} and #{req.last} but current is #{current.join('.')}")
204 raise PluginRequirementError.new("#{id} plugin requires a Redmine version between #{req.first} and #{req.last} but current is #{current.join('.')}")
199 end
205 end
200 else
206 else
201 raise ArgumentError.new(":version option accepts a version string, an array or a range of versions")
207 raise ArgumentError.new(":version option accepts a version string, an array or a range of versions")
202 end
208 end
203 end
209 end
204 end
210 end
205 true
211 true
206 end
212 end
207
213
208 def compare_versions(requirement, current)
214 def compare_versions(requirement, current)
209 requirement = requirement.split('.').collect(&:to_i)
215 requirement = requirement.split('.').collect(&:to_i)
210 requirement <=> current.slice(0, requirement.size)
216 requirement <=> current.slice(0, requirement.size)
211 end
217 end
212 private :compare_versions
218 private :compare_versions
213
219
214 # Sets a requirement on a Redmine plugin version
220 # Sets a requirement on a Redmine plugin version
215 # Raises a PluginRequirementError exception if the requirement is not met
221 # Raises a PluginRequirementError exception if the requirement is not met
216 #
222 #
217 # Examples
223 # Examples
218 # # Requires a plugin named :foo version 0.7.3 or higher
224 # # Requires a plugin named :foo version 0.7.3 or higher
219 # requires_redmine_plugin :foo, :version_or_higher => '0.7.3'
225 # requires_redmine_plugin :foo, :version_or_higher => '0.7.3'
220 # requires_redmine_plugin :foo, '0.7.3'
226 # requires_redmine_plugin :foo, '0.7.3'
221 #
227 #
222 # # Requires a specific version of a Redmine plugin
228 # # Requires a specific version of a Redmine plugin
223 # requires_redmine_plugin :foo, :version => '0.7.3' # 0.7.3 only
229 # requires_redmine_plugin :foo, :version => '0.7.3' # 0.7.3 only
224 # requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
230 # requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
225 def requires_redmine_plugin(plugin_name, arg)
231 def requires_redmine_plugin(plugin_name, arg)
226 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
232 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
227 arg.assert_valid_keys(:version, :version_or_higher)
233 arg.assert_valid_keys(:version, :version_or_higher)
228
234
229 plugin = Plugin.find(plugin_name)
235 plugin = Plugin.find(plugin_name)
230 current = plugin.version.split('.').collect(&:to_i)
236 current = plugin.version.split('.').collect(&:to_i)
231
237
232 arg.each do |k, v|
238 arg.each do |k, v|
233 v = [] << v unless v.is_a?(Array)
239 v = [] << v unless v.is_a?(Array)
234 versions = v.collect {|s| s.split('.').collect(&:to_i)}
240 versions = v.collect {|s| s.split('.').collect(&:to_i)}
235 case k
241 case k
236 when :version_or_higher
242 when :version_or_higher
237 raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
243 raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
238 unless (current <=> versions.first) >= 0
244 unless (current <=> versions.first) >= 0
239 raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin #{v} or higher but current is #{current.join('.')}")
245 raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin #{v} or higher but current is #{current.join('.')}")
240 end
246 end
241 when :version
247 when :version
242 unless versions.include?(current.slice(0,3))
248 unless versions.include?(current.slice(0,3))
243 raise PluginRequirementError.new("#{id} plugin requires one the following versions of #{plugin_name}: #{v.join(', ')} but current is #{current.join('.')}")
249 raise PluginRequirementError.new("#{id} plugin requires one the following versions of #{plugin_name}: #{v.join(', ')} but current is #{current.join('.')}")
244 end
250 end
245 end
251 end
246 end
252 end
247 true
253 true
248 end
254 end
249
255
250 # Adds an item to the given +menu+.
256 # Adds an item to the given +menu+.
251 # The +id+ parameter (equals to the project id) is automatically added to the url.
257 # The +id+ parameter (equals to the project id) is automatically added to the url.
252 # menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
258 # menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
253 #
259 #
254 # +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu
260 # +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu
255 #
261 #
256 def menu(menu, item, url, options={})
262 def menu(menu, item, url, options={})
257 Redmine::MenuManager.map(menu).push(item, url, options)
263 Redmine::MenuManager.map(menu).push(item, url, options)
258 end
264 end
259 alias :add_menu_item :menu
265 alias :add_menu_item :menu
260
266
261 # Removes +item+ from the given +menu+.
267 # Removes +item+ from the given +menu+.
262 def delete_menu_item(menu, item)
268 def delete_menu_item(menu, item)
263 Redmine::MenuManager.map(menu).delete(item)
269 Redmine::MenuManager.map(menu).delete(item)
264 end
270 end
265
271
266 # Defines a permission called +name+ for the given +actions+.
272 # Defines a permission called +name+ for the given +actions+.
267 #
273 #
268 # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array):
274 # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array):
269 # permission :destroy_contacts, { :contacts => :destroy }
275 # permission :destroy_contacts, { :contacts => :destroy }
270 # permission :view_contacts, { :contacts => [:index, :show] }
276 # permission :view_contacts, { :contacts => [:index, :show] }
271 #
277 #
272 # The +options+ argument is a hash that accept the following keys:
278 # The +options+ argument is a hash that accept the following keys:
273 # * :public => the permission is public if set to true (implicitly given to any user)
279 # * :public => the permission is public if set to true (implicitly given to any user)
274 # * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member
280 # * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member
275 # * :read => set it to true so that the permission is still granted on closed projects
281 # * :read => set it to true so that the permission is still granted on closed projects
276 #
282 #
277 # Examples
283 # Examples
278 # # A permission that is implicitly given to any user
284 # # A permission that is implicitly given to any user
279 # # This permission won't appear on the Roles & Permissions setup screen
285 # # This permission won't appear on the Roles & Permissions setup screen
280 # permission :say_hello, { :example => :say_hello }, :public => true, :read => true
286 # permission :say_hello, { :example => :say_hello }, :public => true, :read => true
281 #
287 #
282 # # A permission that can be given to any user
288 # # A permission that can be given to any user
283 # permission :say_hello, { :example => :say_hello }
289 # permission :say_hello, { :example => :say_hello }
284 #
290 #
285 # # A permission that can be given to registered users only
291 # # A permission that can be given to registered users only
286 # permission :say_hello, { :example => :say_hello }, :require => :loggedin
292 # permission :say_hello, { :example => :say_hello }, :require => :loggedin
287 #
293 #
288 # # A permission that can be given to project members only
294 # # A permission that can be given to project members only
289 # permission :say_hello, { :example => :say_hello }, :require => :member
295 # permission :say_hello, { :example => :say_hello }, :require => :member
290 def permission(name, actions, options = {})
296 def permission(name, actions, options = {})
291 if @project_module
297 if @project_module
292 Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
298 Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
293 else
299 else
294 Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
300 Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
295 end
301 end
296 end
302 end
297
303
298 # Defines a project module, that can be enabled/disabled for each project.
304 # Defines a project module, that can be enabled/disabled for each project.
299 # Permissions defined inside +block+ will be bind to the module.
305 # Permissions defined inside +block+ will be bind to the module.
300 #
306 #
301 # project_module :things do
307 # project_module :things do
302 # permission :view_contacts, { :contacts => [:list, :show] }, :public => true
308 # permission :view_contacts, { :contacts => [:list, :show] }, :public => true
303 # permission :destroy_contacts, { :contacts => :destroy }
309 # permission :destroy_contacts, { :contacts => :destroy }
304 # end
310 # end
305 def project_module(name, &block)
311 def project_module(name, &block)
306 @project_module = name
312 @project_module = name
307 self.instance_eval(&block)
313 self.instance_eval(&block)
308 @project_module = nil
314 @project_module = nil
309 end
315 end
310
316
311 # Registers an activity provider.
317 # Registers an activity provider.
312 #
318 #
313 # Options:
319 # Options:
314 # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
320 # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
315 # * <tt>:default</tt> - setting this option to false will make the events not displayed by default
321 # * <tt>:default</tt> - setting this option to false will make the events not displayed by default
316 #
322 #
317 # A model can provide several activity event types.
323 # A model can provide several activity event types.
318 #
324 #
319 # Examples:
325 # Examples:
320 # register :news
326 # register :news
321 # register :scrums, :class_name => 'Meeting'
327 # register :scrums, :class_name => 'Meeting'
322 # register :issues, :class_name => ['Issue', 'Journal']
328 # register :issues, :class_name => ['Issue', 'Journal']
323 #
329 #
324 # Retrieving events:
330 # Retrieving events:
325 # Associated model(s) must implement the find_events class method.
331 # Associated model(s) must implement the find_events class method.
326 # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
332 # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
327 #
333 #
328 # The following call should return all the scrum events visible by current user that occured in the 5 last days:
334 # The following call should return all the scrum events visible by current user that occured in the 5 last days:
329 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
335 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
330 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
336 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
331 #
337 #
332 # Note that :view_scrums permission is required to view these events in the activity view.
338 # Note that :view_scrums permission is required to view these events in the activity view.
333 def activity_provider(*args)
339 def activity_provider(*args)
334 Redmine::Activity.register(*args)
340 Redmine::Activity.register(*args)
335 end
341 end
336
342
337 # Registers a wiki formatter.
343 # Registers a wiki formatter.
338 #
344 #
339 # Parameters:
345 # Parameters:
340 # * +name+ - human-readable name
346 # * +name+ - human-readable name
341 # * +formatter+ - formatter class, which should have an instance method +to_html+
347 # * +formatter+ - formatter class, which should have an instance method +to_html+
342 # * +helper+ - helper module, which will be included by wiki pages
348 # * +helper+ - helper module, which will be included by wiki pages
343 def wiki_format_provider(name, formatter, helper)
349 def wiki_format_provider(name, formatter, helper)
344 Redmine::WikiFormatting.register(name, formatter, helper)
350 Redmine::WikiFormatting.register(name, formatter, helper)
345 end
351 end
346
352
347 # Returns +true+ if the plugin can be configured.
353 # Returns +true+ if the plugin can be configured.
348 def configurable?
354 def configurable?
349 settings && settings.is_a?(Hash) && !settings[:partial].blank?
355 settings && settings.is_a?(Hash) && !settings[:partial].blank?
350 end
356 end
351
357
352 def mirror_assets
358 def mirror_assets
353 source = assets_directory
359 source = assets_directory
354 destination = public_directory
360 destination = public_directory
355 return unless File.directory?(source)
361 return unless File.directory?(source)
356
362
357 source_files = Dir[source + "/**/*"]
363 source_files = Dir[source + "/**/*"]
358 source_dirs = source_files.select { |d| File.directory?(d) }
364 source_dirs = source_files.select { |d| File.directory?(d) }
359 source_files -= source_dirs
365 source_files -= source_dirs
360
366
361 unless source_files.empty?
367 unless source_files.empty?
362 base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
368 base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
363 begin
369 begin
364 FileUtils.mkdir_p(base_target_dir)
370 FileUtils.mkdir_p(base_target_dir)
365 rescue Exception => e
371 rescue Exception => e
366 raise "Could not create directory #{base_target_dir}: " + e.message
372 raise "Could not create directory #{base_target_dir}: " + e.message
367 end
373 end
368 end
374 end
369
375
370 source_dirs.each do |dir|
376 source_dirs.each do |dir|
371 # strip down these paths so we have simple, relative paths we can
377 # strip down these paths so we have simple, relative paths we can
372 # add to the destination
378 # add to the destination
373 target_dir = File.join(destination, dir.gsub(source, ''))
379 target_dir = File.join(destination, dir.gsub(source, ''))
374 begin
380 begin
375 FileUtils.mkdir_p(target_dir)
381 FileUtils.mkdir_p(target_dir)
376 rescue Exception => e
382 rescue Exception => e
377 raise "Could not create directory #{target_dir}: " + e.message
383 raise "Could not create directory #{target_dir}: " + e.message
378 end
384 end
379 end
385 end
380
386
381 source_files.each do |file|
387 source_files.each do |file|
382 begin
388 begin
383 target = File.join(destination, file.gsub(source, ''))
389 target = File.join(destination, file.gsub(source, ''))
384 unless File.exist?(target) && FileUtils.identical?(file, target)
390 unless File.exist?(target) && FileUtils.identical?(file, target)
385 FileUtils.cp(file, target)
391 FileUtils.cp(file, target)
386 end
392 end
387 rescue Exception => e
393 rescue Exception => e
388 raise "Could not copy #{file} to #{target}: " + e.message
394 raise "Could not copy #{file} to #{target}: " + e.message
389 end
395 end
390 end
396 end
391 end
397 end
392
398
393 # Mirrors assets from one or all plugins to public/plugin_assets
399 # Mirrors assets from one or all plugins to public/plugin_assets
394 def self.mirror_assets(name=nil)
400 def self.mirror_assets(name=nil)
395 if name.present?
401 if name.present?
396 find(name).mirror_assets
402 find(name).mirror_assets
397 else
403 else
398 all.each do |plugin|
404 all.each do |plugin|
399 plugin.mirror_assets
405 plugin.mirror_assets
400 end
406 end
401 end
407 end
402 end
408 end
403
409
404 # The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
410 # The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
405 def migration_directory
411 def migration_directory
406 File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate')
412 File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate')
407 end
413 end
408
414
409 # Returns the version number of the latest migration for this plugin. Returns
415 # Returns the version number of the latest migration for this plugin. Returns
410 # nil if this plugin has no migrations.
416 # nil if this plugin has no migrations.
411 def latest_migration
417 def latest_migration
412 migrations.last
418 migrations.last
413 end
419 end
414
420
415 # Returns the version numbers of all migrations for this plugin.
421 # Returns the version numbers of all migrations for this plugin.
416 def migrations
422 def migrations
417 migrations = Dir[migration_directory+"/*.rb"]
423 migrations = Dir[migration_directory+"/*.rb"]
418 migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort
424 migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort
419 end
425 end
420
426
421 # Migrate this plugin to the given version
427 # Migrate this plugin to the given version
422 def migrate(version = nil)
428 def migrate(version = nil)
423 puts "Migrating #{id} (#{name})..."
429 puts "Migrating #{id} (#{name})..."
424 Redmine::Plugin::Migrator.migrate_plugin(self, version)
430 Redmine::Plugin::Migrator.migrate_plugin(self, version)
425 end
431 end
426
432
427 # Migrates all plugins or a single plugin to a given version
433 # Migrates all plugins or a single plugin to a given version
428 # Exemples:
434 # Exemples:
429 # Plugin.migrate
435 # Plugin.migrate
430 # Plugin.migrate('sample_plugin')
436 # Plugin.migrate('sample_plugin')
431 # Plugin.migrate('sample_plugin', 1)
437 # Plugin.migrate('sample_plugin', 1)
432 #
438 #
433 def self.migrate(name=nil, version=nil)
439 def self.migrate(name=nil, version=nil)
434 if name.present?
440 if name.present?
435 find(name).migrate(version)
441 find(name).migrate(version)
436 else
442 else
437 all.each do |plugin|
443 all.each do |plugin|
438 plugin.migrate
444 plugin.migrate
439 end
445 end
440 end
446 end
441 end
447 end
442
448
443 class Migrator < ActiveRecord::Migrator
449 class Migrator < ActiveRecord::Migrator
444 # We need to be able to set the 'current' plugin being migrated.
450 # We need to be able to set the 'current' plugin being migrated.
445 cattr_accessor :current_plugin
451 cattr_accessor :current_plugin
446
452
447 class << self
453 class << self
448 # Runs the migrations from a plugin, up (or down) to the version given
454 # Runs the migrations from a plugin, up (or down) to the version given
449 def migrate_plugin(plugin, version)
455 def migrate_plugin(plugin, version)
450 self.current_plugin = plugin
456 self.current_plugin = plugin
451 return if current_version(plugin) == version
457 return if current_version(plugin) == version
452 migrate(plugin.migration_directory, version)
458 migrate(plugin.migration_directory, version)
453 end
459 end
454
460
455 def current_version(plugin=current_plugin)
461 def current_version(plugin=current_plugin)
456 # Delete migrations that don't match .. to_i will work because the number comes first
462 # Delete migrations that don't match .. to_i will work because the number comes first
457 ::ActiveRecord::Base.connection.select_values(
463 ::ActiveRecord::Base.connection.select_values(
458 "SELECT version FROM #{schema_migrations_table_name}"
464 "SELECT version FROM #{schema_migrations_table_name}"
459 ).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0
465 ).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0
460 end
466 end
461 end
467 end
462
468
463 def migrated
469 def migrated
464 sm_table = self.class.schema_migrations_table_name
470 sm_table = self.class.schema_migrations_table_name
465 ::ActiveRecord::Base.connection.select_values(
471 ::ActiveRecord::Base.connection.select_values(
466 "SELECT version FROM #{sm_table}"
472 "SELECT version FROM #{sm_table}"
467 ).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort
473 ).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort
468 end
474 end
469
475
470 def record_version_state_after_migrating(version)
476 def record_version_state_after_migrating(version)
471 super(version.to_s + "-" + current_plugin.id.to_s)
477 super(version.to_s + "-" + current_plugin.id.to_s)
472 end
478 end
473 end
479 end
474 end
480 end
475 end
481 end
@@ -1,186 +1,189
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 :users
21 fixtures :users
22
22
23 def setup
23 def setup
24 User.current = nil
24 User.current = nil
25 @request.session[:user_id] = 1 # admin
25 @request.session[:user_id] = 1 # admin
26 end
26 end
27
27
28 def test_index
28 def test_index
29 get :index
29 get :index
30 assert_response :success
30 assert_response :success
31 assert_template 'edit'
31 assert_template 'edit'
32 end
32 end
33
33
34 def test_get_edit
34 def test_get_edit
35 get :edit
35 get :edit
36 assert_response :success
36 assert_response :success
37 assert_template 'edit'
37 assert_template 'edit'
38
38
39 assert_tag 'input', :attributes => {:name => 'settings[enabled_scm][]', :value => ''}
39 assert_tag 'input', :attributes => {:name => 'settings[enabled_scm][]', :value => ''}
40 end
40 end
41
41
42 def test_get_edit_should_preselect_default_issue_list_columns
42 def test_get_edit_should_preselect_default_issue_list_columns
43 with_settings :issue_list_default_columns => %w(tracker subject status updated_on) do
43 with_settings :issue_list_default_columns => %w(tracker subject status updated_on) do
44 get :edit
44 get :edit
45 assert_response :success
45 assert_response :success
46 end
46 end
47
47
48 assert_select 'select[id=selected_columns][name=?]', 'settings[issue_list_default_columns][]' do
48 assert_select 'select[id=selected_columns][name=?]', 'settings[issue_list_default_columns][]' do
49 assert_select 'option', 4
49 assert_select 'option', 4
50 assert_select 'option[value=tracker]', :text => 'Tracker'
50 assert_select 'option[value=tracker]', :text => 'Tracker'
51 assert_select 'option[value=subject]', :text => 'Subject'
51 assert_select 'option[value=subject]', :text => 'Subject'
52 assert_select 'option[value=status]', :text => 'Status'
52 assert_select 'option[value=status]', :text => 'Status'
53 assert_select 'option[value=updated_on]', :text => 'Updated'
53 assert_select 'option[value=updated_on]', :text => 'Updated'
54 end
54 end
55
55
56 assert_select 'select[id=available_columns]' do
56 assert_select 'select[id=available_columns]' do
57 assert_select 'option[value=tracker]', 0
57 assert_select 'option[value=tracker]', 0
58 assert_select 'option[value=priority]', :text => 'Priority'
58 assert_select 'option[value=priority]', :text => 'Priority'
59 end
59 end
60 end
60 end
61
61
62 def test_get_edit_without_trackers_should_succeed
62 def test_get_edit_without_trackers_should_succeed
63 Tracker.delete_all
63 Tracker.delete_all
64
64
65 get :edit
65 get :edit
66 assert_response :success
66 assert_response :success
67 end
67 end
68
68
69 def test_post_edit_notifications
69 def test_post_edit_notifications
70 post :edit, :settings => {:mail_from => 'functional@test.foo',
70 post :edit, :settings => {:mail_from => 'functional@test.foo',
71 :bcc_recipients => '0',
71 :bcc_recipients => '0',
72 :notified_events => %w(issue_added issue_updated news_added),
72 :notified_events => %w(issue_added issue_updated news_added),
73 :emails_footer => 'Test footer'
73 :emails_footer => 'Test footer'
74 }
74 }
75 assert_redirected_to '/settings'
75 assert_redirected_to '/settings'
76 assert_equal 'functional@test.foo', Setting.mail_from
76 assert_equal 'functional@test.foo', Setting.mail_from
77 assert !Setting.bcc_recipients?
77 assert !Setting.bcc_recipients?
78 assert_equal %w(issue_added issue_updated news_added), Setting.notified_events
78 assert_equal %w(issue_added issue_updated news_added), Setting.notified_events
79 assert_equal 'Test footer', Setting.emails_footer
79 assert_equal 'Test footer', Setting.emails_footer
80 Setting.clear_cache
80 Setting.clear_cache
81 end
81 end
82
82
83 def test_edit_commit_update_keywords
83 def test_edit_commit_update_keywords
84 with_settings :commit_update_keywords => [
84 with_settings :commit_update_keywords => [
85 {"keywords" => "fixes, resolves", "status_id" => "3"},
85 {"keywords" => "fixes, resolves", "status_id" => "3"},
86 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
86 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
87 ] do
87 ] do
88 get :edit
88 get :edit
89 end
89 end
90 assert_response :success
90 assert_response :success
91 assert_select 'tr.commit-keywords', 2
91 assert_select 'tr.commit-keywords', 2
92 assert_select 'tr.commit-keywords:nth-child(1)' do
92 assert_select 'tr.commit-keywords:nth-child(1)' do
93 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves'
93 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves'
94 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
94 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
95 assert_select 'option[value=3][selected=selected]'
95 assert_select 'option[value=3][selected=selected]'
96 end
96 end
97 end
97 end
98 assert_select 'tr.commit-keywords:nth-child(2)' do
98 assert_select 'tr.commit-keywords:nth-child(2)' do
99 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
99 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
100 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
100 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
101 assert_select 'option[value=5][selected=selected]', :text => 'Closed'
101 assert_select 'option[value=5][selected=selected]', :text => 'Closed'
102 end
102 end
103 assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
103 assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
104 assert_select 'option[value=100][selected=selected]', :text => '100 %'
104 assert_select 'option[value=100][selected=selected]', :text => '100 %'
105 end
105 end
106 assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do
106 assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do
107 assert_select 'option[value=2][selected=selected]', :text => 'Feature request'
107 assert_select 'option[value=2][selected=selected]', :text => 'Feature request'
108 end
108 end
109 end
109 end
110 end
110 end
111
111
112 def test_edit_without_commit_update_keywords_should_show_blank_line
112 def test_edit_without_commit_update_keywords_should_show_blank_line
113 with_settings :commit_update_keywords => [] do
113 with_settings :commit_update_keywords => [] do
114 get :edit
114 get :edit
115 end
115 end
116 assert_response :success
116 assert_response :success
117 assert_select 'tr.commit-keywords', 1 do
117 assert_select 'tr.commit-keywords', 1 do
118 assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]'
118 assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]'
119 end
119 end
120 end
120 end
121
121
122 def test_post_edit_commit_update_keywords
122 def test_post_edit_commit_update_keywords
123 post :edit, :settings => {
123 post :edit, :settings => {
124 :commit_update_keywords => {
124 :commit_update_keywords => {
125 :keywords => ["resolves", "closes"],
125 :keywords => ["resolves", "closes"],
126 :status_id => ["3", "5"],
126 :status_id => ["3", "5"],
127 :done_ratio => ["", "100"],
127 :done_ratio => ["", "100"],
128 :if_tracker_id => ["", "2"]
128 :if_tracker_id => ["", "2"]
129 }
129 }
130 }
130 }
131 assert_redirected_to '/settings'
131 assert_redirected_to '/settings'
132 assert_equal([
132 assert_equal([
133 {"keywords" => "resolves", "status_id" => "3"},
133 {"keywords" => "resolves", "status_id" => "3"},
134 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
134 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
135 ], Setting.commit_update_keywords)
135 ], Setting.commit_update_keywords)
136 end
136 end
137
137
138 def test_get_plugin_settings
138 def test_get_plugin_settings
139 Setting.stubs(:plugin_foo).returns({'sample_setting' => 'Plugin setting value'})
139 Setting.stubs(:plugin_foo).returns({'sample_setting' => 'Plugin setting value'})
140 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"))
141 Redmine::Plugin.register :foo do
141 Redmine::Plugin.register :foo do
142 settings :partial => "foo_plugin/foo_plugin_settings"
142 settings :partial => "foo_plugin/foo_plugin_settings"
143 end
143 end
144
144
145 get :plugin, :id => 'foo'
145 get :plugin, :id => 'foo'
146 assert_response :success
146 assert_response :success
147 assert_template 'plugin'
147 assert_template 'plugin'
148 assert_tag 'form', :attributes => {:action => '/settings/plugin/foo'},
148 assert_tag 'form', :attributes => {:action => '/settings/plugin/foo'},
149 :descendant => {:tag => 'input', :attributes => {:name => 'settings[sample_setting]', :value => 'Plugin setting value'}}
149 :descendant => {:tag => 'input', :attributes => {:name => 'settings[sample_setting]', :value => 'Plugin setting value'}}
150
150
151 Redmine::Plugin.clear
151 ensure
152 Redmine::Plugin.unregister(:foo)
152 end
153 end
153
154
154 def test_get_invalid_plugin_settings
155 def test_get_invalid_plugin_settings
155 get :plugin, :id => 'none'
156 get :plugin, :id => 'none'
156 assert_response 404
157 assert_response 404
157 end
158 end
158
159
159 def test_get_non_configurable_plugin_settings
160 def test_get_non_configurable_plugin_settings
160 Redmine::Plugin.register(:foo) {}
161 Redmine::Plugin.register(:foo) {}
161
162
162 get :plugin, :id => 'foo'
163 get :plugin, :id => 'foo'
163 assert_response 404
164 assert_response 404
164
165
165 Redmine::Plugin.clear
166 ensure
167 Redmine::Plugin.unregister(:foo)
166 end
168 end
167
169
168 def test_post_plugin_settings
170 def test_post_plugin_settings
169 Setting.expects(:plugin_foo=).with({'sample_setting' => 'Value'}).returns(true)
171 Setting.expects(:plugin_foo=).with({'sample_setting' => 'Value'}).returns(true)
170 Redmine::Plugin.register(:foo) do
172 Redmine::Plugin.register(:foo) do
171 settings :partial => 'not blank' # so that configurable? is true
173 settings :partial => 'not blank' # so that configurable? is true
172 end
174 end
173
175
174 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
176 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
175 assert_redirected_to '/settings/plugin/foo'
177 assert_redirected_to '/settings/plugin/foo'
176 end
178 end
177
179
178 def test_post_non_configurable_plugin_settings
180 def test_post_non_configurable_plugin_settings
179 Redmine::Plugin.register(:foo) {}
181 Redmine::Plugin.register(:foo) {}
180
182
181 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
183 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
182 assert_response 404
184 assert_response 404
183
185
184 Redmine::Plugin.clear
186 ensure
187 Redmine::Plugin.unregister(:foo)
185 end
188 end
186 end
189 end
General Comments 0
You need to be logged in to leave comments. Login now