##// END OF EJS Templates
Merged r10472 from trunk (#11945)...
Toshi MARUYAMA -
r10267:21a16e09589f
parent child
Show More
@@ -1,447 +1,451
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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
67 def_field :name, :description, :url, :author, :author_url, :version, :settings
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 # Set a default name if it was not provided during registration
74 # Set a default name if it was not provided during registration
75 p.name(id.to_s.humanize) if p.name.nil?
75 p.name(id.to_s.humanize) if p.name.nil?
76
76
77 # Adds plugin locales if any
77 # Adds plugin locales if any
78 # YAML translation files should be found under <plugin>/config/locales/
78 # YAML translation files should be found under <plugin>/config/locales/
79 ::I18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
79 ::I18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
80
80
81 # Prepends the app/views directory of the plugin to the view path
81 # Prepends the app/views directory of the plugin to the view path
82 view_path = File.join(p.directory, 'app', 'views')
82 view_path = File.join(p.directory, 'app', 'views')
83 if File.directory?(view_path)
83 if File.directory?(view_path)
84 ActionController::Base.prepend_view_path(view_path)
84 ActionController::Base.prepend_view_path(view_path)
85 ActionMailer::Base.prepend_view_path(view_path)
85 ActionMailer::Base.prepend_view_path(view_path)
86 end
86 end
87
87
88 # Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
88 # Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
89 Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
89 Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
90 ActiveSupport::Dependencies.autoload_paths += [dir]
90 ActiveSupport::Dependencies.autoload_paths += [dir]
91 end
91 end
92
92
93 registered_plugins[id] = p
93 registered_plugins[id] = p
94 end
94 end
95
95
96 # Returns an array of all registered plugins
96 # Returns an array of all registered plugins
97 def self.all
97 def self.all
98 registered_plugins.values.sort
98 registered_plugins.values.sort
99 end
99 end
100
100
101 # Finds a plugin by its id
101 # Finds a plugin by its id
102 # Returns a PluginNotFound exception if the plugin doesn't exist
102 # Returns a PluginNotFound exception if the plugin doesn't exist
103 def self.find(id)
103 def self.find(id)
104 registered_plugins[id.to_sym] || raise(PluginNotFound)
104 registered_plugins[id.to_sym] || raise(PluginNotFound)
105 end
105 end
106
106
107 # Clears the registered plugins hash
107 # Clears the registered plugins hash
108 # It doesn't unload installed plugins
108 # It doesn't unload installed plugins
109 def self.clear
109 def self.clear
110 @registered_plugins = {}
110 @registered_plugins = {}
111 end
111 end
112
112
113 # Checks if a plugin is installed
113 # Checks if a plugin is installed
114 #
114 #
115 # @param [String] id name of the plugin
115 # @param [String] id name of the plugin
116 def self.installed?(id)
116 def self.installed?(id)
117 registered_plugins[id.to_sym].present?
117 registered_plugins[id.to_sym].present?
118 end
118 end
119
119
120 def self.load
120 def self.load
121 Dir.glob(File.join(self.directory, '*')).sort.each do |directory|
121 Dir.glob(File.join(self.directory, '*')).sort.each do |directory|
122 if File.directory?(directory)
122 if File.directory?(directory)
123 lib = File.join(directory, "lib")
123 lib = File.join(directory, "lib")
124 if File.directory?(lib)
124 if File.directory?(lib)
125 $:.unshift lib
125 $:.unshift lib
126 ActiveSupport::Dependencies.autoload_paths += [lib]
126 ActiveSupport::Dependencies.autoload_paths += [lib]
127 end
127 end
128 initializer = File.join(directory, "init.rb")
128 initializer = File.join(directory, "init.rb")
129 if File.file?(initializer)
129 if File.file?(initializer)
130 require initializer
130 require initializer
131 end
131 end
132 end
132 end
133 end
133 end
134 end
134 end
135
135
136 def initialize(id)
136 def initialize(id)
137 @id = id.to_sym
137 @id = id.to_sym
138 end
138 end
139
139
140 def directory
140 def directory
141 File.join(self.class.directory, id.to_s)
141 File.join(self.class.directory, id.to_s)
142 end
142 end
143
143
144 def public_directory
144 def public_directory
145 File.join(self.class.public_directory, id.to_s)
145 File.join(self.class.public_directory, id.to_s)
146 end
146 end
147
147
148 def assets_directory
148 def assets_directory
149 File.join(directory, 'assets')
149 File.join(directory, 'assets')
150 end
150 end
151
151
152 def <=>(plugin)
152 def <=>(plugin)
153 self.id.to_s <=> plugin.id.to_s
153 self.id.to_s <=> plugin.id.to_s
154 end
154 end
155
155
156 # Sets a requirement on Redmine version
156 # Sets a requirement on Redmine version
157 # Raises a PluginRequirementError exception if the requirement is not met
157 # Raises a PluginRequirementError exception if the requirement is not met
158 #
158 #
159 # Examples
159 # Examples
160 # # Requires Redmine 0.7.3 or higher
160 # # Requires Redmine 0.7.3 or higher
161 # requires_redmine :version_or_higher => '0.7.3'
161 # requires_redmine :version_or_higher => '0.7.3'
162 # requires_redmine '0.7.3'
162 # requires_redmine '0.7.3'
163 #
163 #
164 # # Requires a specific Redmine version
164 # # Requires a specific Redmine version
165 # requires_redmine :version => '0.7.3' # 0.7.3 only
165 # requires_redmine :version => '0.7.3' # 0.7.3 only
166 # requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
166 # requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
167 def requires_redmine(arg)
167 def requires_redmine(arg)
168 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
168 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
169 arg.assert_valid_keys(:version, :version_or_higher)
169 arg.assert_valid_keys(:version, :version_or_higher)
170
170
171 current = Redmine::VERSION.to_a
171 current = Redmine::VERSION.to_a
172 arg.each do |k, v|
172 arg.each do |k, v|
173 v = [] << v unless v.is_a?(Array)
173 v = [] << v unless v.is_a?(Array)
174 versions = v.collect {|s| s.split('.').collect(&:to_i)}
174 versions = v.collect {|s| s.split('.').collect(&:to_i)}
175 case k
175 case k
176 when :version_or_higher
176 when :version_or_higher
177 raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
177 raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
178 unless (current <=> versions.first) >= 0
178 unless (current <=> versions.first) >= 0
179 raise PluginRequirementError.new("#{id} plugin requires Redmine #{v} or higher but current is #{current.join('.')}")
179 raise PluginRequirementError.new("#{id} plugin requires Redmine #{v} or higher but current is #{current.join('.')}")
180 end
180 end
181 when :version
181 when :version
182 unless versions.include?(current.slice(0,3))
182 unless versions.include?(current.slice(0,3))
183 raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{v.join(', ')} but current is #{current.join('.')}")
183 raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{v.join(', ')} but current is #{current.join('.')}")
184 end
184 end
185 end
185 end
186 end
186 end
187 true
187 true
188 end
188 end
189
189
190 # Sets a requirement on a Redmine plugin version
190 # Sets a requirement on a Redmine plugin version
191 # Raises a PluginRequirementError exception if the requirement is not met
191 # Raises a PluginRequirementError exception if the requirement is not met
192 #
192 #
193 # Examples
193 # Examples
194 # # Requires a plugin named :foo version 0.7.3 or higher
194 # # Requires a plugin named :foo version 0.7.3 or higher
195 # requires_redmine_plugin :foo, :version_or_higher => '0.7.3'
195 # requires_redmine_plugin :foo, :version_or_higher => '0.7.3'
196 # requires_redmine_plugin :foo, '0.7.3'
196 # requires_redmine_plugin :foo, '0.7.3'
197 #
197 #
198 # # Requires a specific version of a Redmine plugin
198 # # Requires a specific version of a Redmine plugin
199 # requires_redmine_plugin :foo, :version => '0.7.3' # 0.7.3 only
199 # requires_redmine_plugin :foo, :version => '0.7.3' # 0.7.3 only
200 # requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
200 # requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
201 def requires_redmine_plugin(plugin_name, arg)
201 def requires_redmine_plugin(plugin_name, arg)
202 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
202 arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
203 arg.assert_valid_keys(:version, :version_or_higher)
203 arg.assert_valid_keys(:version, :version_or_higher)
204
204
205 plugin = Plugin.find(plugin_name)
205 plugin = Plugin.find(plugin_name)
206 current = plugin.version.split('.').collect(&:to_i)
206 current = plugin.version.split('.').collect(&:to_i)
207
207
208 arg.each do |k, v|
208 arg.each do |k, v|
209 v = [] << v unless v.is_a?(Array)
209 v = [] << v unless v.is_a?(Array)
210 versions = v.collect {|s| s.split('.').collect(&:to_i)}
210 versions = v.collect {|s| s.split('.').collect(&:to_i)}
211 case k
211 case k
212 when :version_or_higher
212 when :version_or_higher
213 raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
213 raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
214 unless (current <=> versions.first) >= 0
214 unless (current <=> versions.first) >= 0
215 raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin #{v} or higher but current is #{current.join('.')}")
215 raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin #{v} or higher but current is #{current.join('.')}")
216 end
216 end
217 when :version
217 when :version
218 unless versions.include?(current.slice(0,3))
218 unless versions.include?(current.slice(0,3))
219 raise PluginRequirementError.new("#{id} plugin requires one the following versions of #{plugin_name}: #{v.join(', ')} but current is #{current.join('.')}")
219 raise PluginRequirementError.new("#{id} plugin requires one the following versions of #{plugin_name}: #{v.join(', ')} but current is #{current.join('.')}")
220 end
220 end
221 end
221 end
222 end
222 end
223 true
223 true
224 end
224 end
225
225
226 # Adds an item to the given +menu+.
226 # Adds an item to the given +menu+.
227 # The +id+ parameter (equals to the project id) is automatically added to the url.
227 # The +id+ parameter (equals to the project id) is automatically added to the url.
228 # menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
228 # menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
229 #
229 #
230 # +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu
230 # +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu
231 #
231 #
232 def menu(menu, item, url, options={})
232 def menu(menu, item, url, options={})
233 Redmine::MenuManager.map(menu).push(item, url, options)
233 Redmine::MenuManager.map(menu).push(item, url, options)
234 end
234 end
235 alias :add_menu_item :menu
235 alias :add_menu_item :menu
236
236
237 # Removes +item+ from the given +menu+.
237 # Removes +item+ from the given +menu+.
238 def delete_menu_item(menu, item)
238 def delete_menu_item(menu, item)
239 Redmine::MenuManager.map(menu).delete(item)
239 Redmine::MenuManager.map(menu).delete(item)
240 end
240 end
241
241
242 # Defines a permission called +name+ for the given +actions+.
242 # Defines a permission called +name+ for the given +actions+.
243 #
243 #
244 # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array):
244 # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array):
245 # permission :destroy_contacts, { :contacts => :destroy }
245 # permission :destroy_contacts, { :contacts => :destroy }
246 # permission :view_contacts, { :contacts => [:index, :show] }
246 # permission :view_contacts, { :contacts => [:index, :show] }
247 #
247 #
248 # The +options+ argument is a hash that accept the following keys:
248 # The +options+ argument is a hash that accept the following keys:
249 # * :public => the permission is public if set to true (implicitly given to any user)
249 # * :public => the permission is public if set to true (implicitly given to any user)
250 # * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member
250 # * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member
251 # * :read => set it to true so that the permission is still granted on closed projects
251 # * :read => set it to true so that the permission is still granted on closed projects
252 #
252 #
253 # Examples
253 # Examples
254 # # A permission that is implicitly given to any user
254 # # A permission that is implicitly given to any user
255 # # This permission won't appear on the Roles & Permissions setup screen
255 # # This permission won't appear on the Roles & Permissions setup screen
256 # permission :say_hello, { :example => :say_hello }, :public => true, :read => true
256 # permission :say_hello, { :example => :say_hello }, :public => true, :read => true
257 #
257 #
258 # # A permission that can be given to any user
258 # # A permission that can be given to any user
259 # permission :say_hello, { :example => :say_hello }
259 # permission :say_hello, { :example => :say_hello }
260 #
260 #
261 # # A permission that can be given to registered users only
261 # # A permission that can be given to registered users only
262 # permission :say_hello, { :example => :say_hello }, :require => :loggedin
262 # permission :say_hello, { :example => :say_hello }, :require => :loggedin
263 #
263 #
264 # # A permission that can be given to project members only
264 # # A permission that can be given to project members only
265 # permission :say_hello, { :example => :say_hello }, :require => :member
265 # permission :say_hello, { :example => :say_hello }, :require => :member
266 def permission(name, actions, options = {})
266 def permission(name, actions, options = {})
267 if @project_module
267 if @project_module
268 Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
268 Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
269 else
269 else
270 Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
270 Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
271 end
271 end
272 end
272 end
273
273
274 # Defines a project module, that can be enabled/disabled for each project.
274 # Defines a project module, that can be enabled/disabled for each project.
275 # Permissions defined inside +block+ will be bind to the module.
275 # Permissions defined inside +block+ will be bind to the module.
276 #
276 #
277 # project_module :things do
277 # project_module :things do
278 # permission :view_contacts, { :contacts => [:list, :show] }, :public => true
278 # permission :view_contacts, { :contacts => [:list, :show] }, :public => true
279 # permission :destroy_contacts, { :contacts => :destroy }
279 # permission :destroy_contacts, { :contacts => :destroy }
280 # end
280 # end
281 def project_module(name, &block)
281 def project_module(name, &block)
282 @project_module = name
282 @project_module = name
283 self.instance_eval(&block)
283 self.instance_eval(&block)
284 @project_module = nil
284 @project_module = nil
285 end
285 end
286
286
287 # Registers an activity provider.
287 # Registers an activity provider.
288 #
288 #
289 # Options:
289 # Options:
290 # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
290 # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
291 # * <tt>:default</tt> - setting this option to false will make the events not displayed by default
291 # * <tt>:default</tt> - setting this option to false will make the events not displayed by default
292 #
292 #
293 # A model can provide several activity event types.
293 # A model can provide several activity event types.
294 #
294 #
295 # Examples:
295 # Examples:
296 # register :news
296 # register :news
297 # register :scrums, :class_name => 'Meeting'
297 # register :scrums, :class_name => 'Meeting'
298 # register :issues, :class_name => ['Issue', 'Journal']
298 # register :issues, :class_name => ['Issue', 'Journal']
299 #
299 #
300 # Retrieving events:
300 # Retrieving events:
301 # Associated model(s) must implement the find_events class method.
301 # Associated model(s) must implement the find_events class method.
302 # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
302 # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
303 #
303 #
304 # The following call should return all the scrum events visible by current user that occured in the 5 last days:
304 # The following call should return all the scrum events visible by current user that occured in the 5 last days:
305 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
305 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
306 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
306 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
307 #
307 #
308 # Note that :view_scrums permission is required to view these events in the activity view.
308 # Note that :view_scrums permission is required to view these events in the activity view.
309 def activity_provider(*args)
309 def activity_provider(*args)
310 Redmine::Activity.register(*args)
310 Redmine::Activity.register(*args)
311 end
311 end
312
312
313 # Registers a wiki formatter.
313 # Registers a wiki formatter.
314 #
314 #
315 # Parameters:
315 # Parameters:
316 # * +name+ - human-readable name
316 # * +name+ - human-readable name
317 # * +formatter+ - formatter class, which should have an instance method +to_html+
317 # * +formatter+ - formatter class, which should have an instance method +to_html+
318 # * +helper+ - helper module, which will be included by wiki pages
318 # * +helper+ - helper module, which will be included by wiki pages
319 def wiki_format_provider(name, formatter, helper)
319 def wiki_format_provider(name, formatter, helper)
320 Redmine::WikiFormatting.register(name, formatter, helper)
320 Redmine::WikiFormatting.register(name, formatter, helper)
321 end
321 end
322
322
323 # Returns +true+ if the plugin can be configured.
323 # Returns +true+ if the plugin can be configured.
324 def configurable?
324 def configurable?
325 settings && settings.is_a?(Hash) && !settings[:partial].blank?
325 settings && settings.is_a?(Hash) && !settings[:partial].blank?
326 end
326 end
327
327
328 def mirror_assets
328 def mirror_assets
329 source = assets_directory
329 source = assets_directory
330 destination = public_directory
330 destination = public_directory
331 return unless File.directory?(source)
331 return unless File.directory?(source)
332
332
333 source_files = Dir[source + "/**/*"]
333 source_files = Dir[source + "/**/*"]
334 source_dirs = source_files.select { |d| File.directory?(d) }
334 source_dirs = source_files.select { |d| File.directory?(d) }
335 source_files -= source_dirs
335 source_files -= source_dirs
336
336
337 unless source_files.empty?
337 unless source_files.empty?
338 base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
338 base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
339 FileUtils.mkdir_p(base_target_dir)
339 begin
340 FileUtils.mkdir_p(base_target_dir)
341 rescue Exception => e
342 raise "Could not create directory #{base_target_dir}: " + e.message
343 end
340 end
344 end
341
345
342 source_dirs.each do |dir|
346 source_dirs.each do |dir|
343 # strip down these paths so we have simple, relative paths we can
347 # strip down these paths so we have simple, relative paths we can
344 # add to the destination
348 # add to the destination
345 target_dir = File.join(destination, dir.gsub(source, ''))
349 target_dir = File.join(destination, dir.gsub(source, ''))
346 begin
350 begin
347 FileUtils.mkdir_p(target_dir)
351 FileUtils.mkdir_p(target_dir)
348 rescue Exception => e
352 rescue Exception => e
349 raise "Could not create directory #{target_dir}: " + e.message
353 raise "Could not create directory #{target_dir}: " + e.message
350 end
354 end
351 end
355 end
352
356
353 source_files.each do |file|
357 source_files.each do |file|
354 begin
358 begin
355 target = File.join(destination, file.gsub(source, ''))
359 target = File.join(destination, file.gsub(source, ''))
356 unless File.exist?(target) && FileUtils.identical?(file, target)
360 unless File.exist?(target) && FileUtils.identical?(file, target)
357 FileUtils.cp(file, target)
361 FileUtils.cp(file, target)
358 end
362 end
359 rescue Exception => e
363 rescue Exception => e
360 raise "Could not copy #{file} to #{target}: " + e.message
364 raise "Could not copy #{file} to #{target}: " + e.message
361 end
365 end
362 end
366 end
363 end
367 end
364
368
365 # Mirrors assets from one or all plugins to public/plugin_assets
369 # Mirrors assets from one or all plugins to public/plugin_assets
366 def self.mirror_assets(name=nil)
370 def self.mirror_assets(name=nil)
367 if name.present?
371 if name.present?
368 find(name).mirror_assets
372 find(name).mirror_assets
369 else
373 else
370 all.each do |plugin|
374 all.each do |plugin|
371 plugin.mirror_assets
375 plugin.mirror_assets
372 end
376 end
373 end
377 end
374 end
378 end
375
379
376 # The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
380 # The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
377 def migration_directory
381 def migration_directory
378 File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate')
382 File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate')
379 end
383 end
380
384
381 # Returns the version number of the latest migration for this plugin. Returns
385 # Returns the version number of the latest migration for this plugin. Returns
382 # nil if this plugin has no migrations.
386 # nil if this plugin has no migrations.
383 def latest_migration
387 def latest_migration
384 migrations.last
388 migrations.last
385 end
389 end
386
390
387 # Returns the version numbers of all migrations for this plugin.
391 # Returns the version numbers of all migrations for this plugin.
388 def migrations
392 def migrations
389 migrations = Dir[migration_directory+"/*.rb"]
393 migrations = Dir[migration_directory+"/*.rb"]
390 migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort
394 migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort
391 end
395 end
392
396
393 # Migrate this plugin to the given version
397 # Migrate this plugin to the given version
394 def migrate(version = nil)
398 def migrate(version = nil)
395 puts "Migrating #{id} (#{name})..."
399 puts "Migrating #{id} (#{name})..."
396 Redmine::Plugin::Migrator.migrate_plugin(self, version)
400 Redmine::Plugin::Migrator.migrate_plugin(self, version)
397 end
401 end
398
402
399 # Migrates all plugins or a single plugin to a given version
403 # Migrates all plugins or a single plugin to a given version
400 # Exemples:
404 # Exemples:
401 # Plugin.migrate
405 # Plugin.migrate
402 # Plugin.migrate('sample_plugin')
406 # Plugin.migrate('sample_plugin')
403 # Plugin.migrate('sample_plugin', 1)
407 # Plugin.migrate('sample_plugin', 1)
404 #
408 #
405 def self.migrate(name=nil, version=nil)
409 def self.migrate(name=nil, version=nil)
406 if name.present?
410 if name.present?
407 find(name).migrate(version)
411 find(name).migrate(version)
408 else
412 else
409 all.each do |plugin|
413 all.each do |plugin|
410 plugin.migrate
414 plugin.migrate
411 end
415 end
412 end
416 end
413 end
417 end
414
418
415 class Migrator < ActiveRecord::Migrator
419 class Migrator < ActiveRecord::Migrator
416 # We need to be able to set the 'current' plugin being migrated.
420 # We need to be able to set the 'current' plugin being migrated.
417 cattr_accessor :current_plugin
421 cattr_accessor :current_plugin
418
422
419 class << self
423 class << self
420 # Runs the migrations from a plugin, up (or down) to the version given
424 # Runs the migrations from a plugin, up (or down) to the version given
421 def migrate_plugin(plugin, version)
425 def migrate_plugin(plugin, version)
422 self.current_plugin = plugin
426 self.current_plugin = plugin
423 return if current_version(plugin) == version
427 return if current_version(plugin) == version
424 migrate(plugin.migration_directory, version)
428 migrate(plugin.migration_directory, version)
425 end
429 end
426
430
427 def current_version(plugin=current_plugin)
431 def current_version(plugin=current_plugin)
428 # Delete migrations that don't match .. to_i will work because the number comes first
432 # Delete migrations that don't match .. to_i will work because the number comes first
429 ::ActiveRecord::Base.connection.select_values(
433 ::ActiveRecord::Base.connection.select_values(
430 "SELECT version FROM #{schema_migrations_table_name}"
434 "SELECT version FROM #{schema_migrations_table_name}"
431 ).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0
435 ).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0
432 end
436 end
433 end
437 end
434
438
435 def migrated
439 def migrated
436 sm_table = self.class.schema_migrations_table_name
440 sm_table = self.class.schema_migrations_table_name
437 ::ActiveRecord::Base.connection.select_values(
441 ::ActiveRecord::Base.connection.select_values(
438 "SELECT version FROM #{sm_table}"
442 "SELECT version FROM #{sm_table}"
439 ).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort
443 ).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort
440 end
444 end
441
445
442 def record_version_state_after_migrating(version)
446 def record_version_state_after_migrating(version)
443 super(version.to_s + "-" + current_plugin.id.to_s)
447 super(version.to_s + "-" + current_plugin.id.to_s)
444 end
448 end
445 end
449 end
446 end
450 end
447 end
451 end
General Comments 0
You need to be logged in to leave comments. Login now