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