Mercurial > hg > soundsoftware-site
comparison lib/redmine/plugin.rb @ 1115:433d4f72a19b redmine-2.2
Update to Redmine SVN revision 11137 on 2.2-stable branch
author | Chris Cannam |
---|---|
date | Mon, 07 Jan 2013 12:01:42 +0000 |
parents | cbb26bc654de |
children | 622f24f53b42 261b3d9a4903 |
comparison
equal
deleted
inserted
replaced
929:5f33065ddc4b | 1115:433d4f72a19b |
---|---|
1 # Redmine - project management software | 1 # Redmine - project management software |
2 # Copyright (C) 2006-2011 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. |
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 | |
47 self.directory = File.join(Rails.root, 'plugins') | |
48 | |
49 cattr_accessor :public_directory | |
50 self.public_directory = File.join(Rails.root, 'public', 'plugin_assets') | |
51 | |
46 @registered_plugins = {} | 52 @registered_plugins = {} |
47 class << self | 53 class << self |
48 attr_reader :registered_plugins | 54 attr_reader :registered_plugins |
49 private :new | 55 private :new |
50 | 56 |
65 def self.register(id, &block) | 71 def self.register(id, &block) |
66 p = new(id) | 72 p = new(id) |
67 p.instance_eval(&block) | 73 p.instance_eval(&block) |
68 # Set a default name if it was not provided during registration | 74 # Set a default name if it was not provided during registration |
69 p.name(id.to_s.humanize) if p.name.nil? | 75 p.name(id.to_s.humanize) if p.name.nil? |
76 | |
70 # Adds plugin locales if any | 77 # Adds plugin locales if any |
71 # YAML translation files should be found under <plugin>/config/locales/ | 78 # YAML translation files should be found under <plugin>/config/locales/ |
72 ::I18n.load_path += Dir.glob(File.join(Rails.root, 'vendor', 'plugins', id.to_s, 'config', 'locales', '*.yml')) | 79 ::I18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml')) |
80 | |
81 # Prepends the app/views directory of the plugin to the view path | |
82 view_path = File.join(p.directory, 'app', 'views') | |
83 if File.directory?(view_path) | |
84 ActionController::Base.prepend_view_path(view_path) | |
85 ActionMailer::Base.prepend_view_path(view_path) | |
86 end | |
87 | |
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| | |
90 ActiveSupport::Dependencies.autoload_paths += [dir] | |
91 end | |
92 | |
73 registered_plugins[id] = p | 93 registered_plugins[id] = p |
74 end | 94 end |
75 | 95 |
76 # Returns an array of all registered plugins | 96 # Returns an array of all registered plugins |
77 def self.all | 97 def self.all |
95 # @param [String] id name of the plugin | 115 # @param [String] id name of the plugin |
96 def self.installed?(id) | 116 def self.installed?(id) |
97 registered_plugins[id.to_sym].present? | 117 registered_plugins[id.to_sym].present? |
98 end | 118 end |
99 | 119 |
120 def self.load | |
121 Dir.glob(File.join(self.directory, '*')).sort.each do |directory| | |
122 if File.directory?(directory) | |
123 lib = File.join(directory, "lib") | |
124 if File.directory?(lib) | |
125 $:.unshift lib | |
126 ActiveSupport::Dependencies.autoload_paths += [lib] | |
127 end | |
128 initializer = File.join(directory, "init.rb") | |
129 if File.file?(initializer) | |
130 require initializer | |
131 end | |
132 end | |
133 end | |
134 end | |
135 | |
100 def initialize(id) | 136 def initialize(id) |
101 @id = id.to_sym | 137 @id = id.to_sym |
138 end | |
139 | |
140 def directory | |
141 File.join(self.class.directory, id.to_s) | |
142 end | |
143 | |
144 def public_directory | |
145 File.join(self.class.public_directory, id.to_s) | |
146 end | |
147 | |
148 def assets_directory | |
149 File.join(directory, 'assets') | |
102 end | 150 end |
103 | 151 |
104 def <=>(plugin) | 152 def <=>(plugin) |
105 self.id.to_s <=> plugin.id.to_s | 153 self.id.to_s <=> plugin.id.to_s |
106 end | 154 end |
111 # Examples | 159 # Examples |
112 # # Requires Redmine 0.7.3 or higher | 160 # # Requires Redmine 0.7.3 or higher |
113 # requires_redmine :version_or_higher => '0.7.3' | 161 # requires_redmine :version_or_higher => '0.7.3' |
114 # requires_redmine '0.7.3' | 162 # requires_redmine '0.7.3' |
115 # | 163 # |
164 # # Requires Redmine 0.7.x or higher | |
165 # requires_redmine '0.7' | |
166 # | |
116 # # Requires a specific Redmine version | 167 # # Requires a specific Redmine version |
117 # requires_redmine :version => '0.7.3' # 0.7.3 only | 168 # requires_redmine :version => '0.7.3' # 0.7.3 only |
169 # requires_redmine :version => '0.7' # 0.7.x | |
118 # requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0 | 170 # requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0 |
171 # | |
172 # # Requires a Redmine version within a range | |
173 # requires_redmine :version => '0.7.3'..'0.9.1' # >= 0.7.3 and <= 0.9.1 | |
174 # requires_redmine :version => '0.7'..'0.9' # >= 0.7.x and <= 0.9.x | |
119 def requires_redmine(arg) | 175 def requires_redmine(arg) |
120 arg = { :version_or_higher => arg } unless arg.is_a?(Hash) | 176 arg = { :version_or_higher => arg } unless arg.is_a?(Hash) |
121 arg.assert_valid_keys(:version, :version_or_higher) | 177 arg.assert_valid_keys(:version, :version_or_higher) |
122 | 178 |
123 current = Redmine::VERSION.to_a | 179 current = Redmine::VERSION.to_a |
124 arg.each do |k, v| | 180 arg.each do |k, req| |
125 v = [] << v unless v.is_a?(Array) | |
126 versions = v.collect {|s| s.split('.').collect(&:to_i)} | |
127 case k | 181 case k |
128 when :version_or_higher | 182 when :version_or_higher |
129 raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1 | 183 raise ArgumentError.new(":version_or_higher accepts a version string only") unless req.is_a?(String) |
130 unless (current <=> versions.first) >= 0 | 184 unless compare_versions(req, current) <= 0 |
131 raise PluginRequirementError.new("#{id} plugin requires Redmine #{v} or higher but current is #{current.join('.')}") | 185 raise PluginRequirementError.new("#{id} plugin requires Redmine #{req} or higher but current is #{current.join('.')}") |
132 end | 186 end |
133 when :version | 187 when :version |
134 unless versions.include?(current.slice(0,3)) | 188 req = [req] if req.is_a?(String) |
135 raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{v.join(', ')} but current is #{current.join('.')}") | 189 if req.is_a?(Array) |
190 unless req.detect {|ver| compare_versions(ver, current) == 0} | |
191 raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{req.join(', ')} but current is #{current.join('.')}") | |
192 end | |
193 elsif req.is_a?(Range) | |
194 unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0 | |
195 raise PluginRequirementError.new("#{id} plugin requires a Redmine version between #{req.first} and #{req.last} but current is #{current.join('.')}") | |
196 end | |
197 else | |
198 raise ArgumentError.new(":version option accepts a version string, an array or a range of versions") | |
136 end | 199 end |
137 end | 200 end |
138 end | 201 end |
139 true | 202 true |
140 end | 203 end |
204 | |
205 def compare_versions(requirement, current) | |
206 requirement = requirement.split('.').collect(&:to_i) | |
207 requirement <=> current.slice(0, requirement.size) | |
208 end | |
209 private :compare_versions | |
141 | 210 |
142 # Sets a requirement on a Redmine plugin version | 211 # Sets a requirement on a Redmine plugin version |
143 # Raises a PluginRequirementError exception if the requirement is not met | 212 # Raises a PluginRequirementError exception if the requirement is not met |
144 # | 213 # |
145 # Examples | 214 # Examples |
195 # | 264 # |
196 # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array): | 265 # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array): |
197 # permission :destroy_contacts, { :contacts => :destroy } | 266 # permission :destroy_contacts, { :contacts => :destroy } |
198 # permission :view_contacts, { :contacts => [:index, :show] } | 267 # permission :view_contacts, { :contacts => [:index, :show] } |
199 # | 268 # |
200 # The +options+ argument can be used to make the permission public (implicitly given to any user) | 269 # The +options+ argument is a hash that accept the following keys: |
201 # or to restrict users the permission can be given to. | 270 # * :public => the permission is public if set to true (implicitly given to any user) |
271 # * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member | |
272 # * :read => set it to true so that the permission is still granted on closed projects | |
202 # | 273 # |
203 # Examples | 274 # Examples |
204 # # A permission that is implicitly given to any user | 275 # # A permission that is implicitly given to any user |
205 # # This permission won't appear on the Roles & Permissions setup screen | 276 # # This permission won't appear on the Roles & Permissions setup screen |
206 # permission :say_hello, { :example => :say_hello }, :public => true | 277 # permission :say_hello, { :example => :say_hello }, :public => true, :read => true |
207 # | 278 # |
208 # # A permission that can be given to any user | 279 # # A permission that can be given to any user |
209 # permission :say_hello, { :example => :say_hello } | 280 # permission :say_hello, { :example => :say_hello } |
210 # | 281 # |
211 # # A permission that can be given to registered users only | 282 # # A permission that can be given to registered users only |
272 | 343 |
273 # Returns +true+ if the plugin can be configured. | 344 # Returns +true+ if the plugin can be configured. |
274 def configurable? | 345 def configurable? |
275 settings && settings.is_a?(Hash) && !settings[:partial].blank? | 346 settings && settings.is_a?(Hash) && !settings[:partial].blank? |
276 end | 347 end |
348 | |
349 def mirror_assets | |
350 source = assets_directory | |
351 destination = public_directory | |
352 return unless File.directory?(source) | |
353 | |
354 source_files = Dir[source + "/**/*"] | |
355 source_dirs = source_files.select { |d| File.directory?(d) } | |
356 source_files -= source_dirs | |
357 | |
358 unless source_files.empty? | |
359 base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, '')) | |
360 begin | |
361 FileUtils.mkdir_p(base_target_dir) | |
362 rescue Exception => e | |
363 raise "Could not create directory #{base_target_dir}: " + e.message | |
364 end | |
365 end | |
366 | |
367 source_dirs.each do |dir| | |
368 # strip down these paths so we have simple, relative paths we can | |
369 # add to the destination | |
370 target_dir = File.join(destination, dir.gsub(source, '')) | |
371 begin | |
372 FileUtils.mkdir_p(target_dir) | |
373 rescue Exception => e | |
374 raise "Could not create directory #{target_dir}: " + e.message | |
375 end | |
376 end | |
377 | |
378 source_files.each do |file| | |
379 begin | |
380 target = File.join(destination, file.gsub(source, '')) | |
381 unless File.exist?(target) && FileUtils.identical?(file, target) | |
382 FileUtils.cp(file, target) | |
383 end | |
384 rescue Exception => e | |
385 raise "Could not copy #{file} to #{target}: " + e.message | |
386 end | |
387 end | |
388 end | |
389 | |
390 # Mirrors assets from one or all plugins to public/plugin_assets | |
391 def self.mirror_assets(name=nil) | |
392 if name.present? | |
393 find(name).mirror_assets | |
394 else | |
395 all.each do |plugin| | |
396 plugin.mirror_assets | |
397 end | |
398 end | |
399 end | |
400 | |
401 # The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>) | |
402 def migration_directory | |
403 File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate') | |
404 end | |
405 | |
406 # Returns the version number of the latest migration for this plugin. Returns | |
407 # nil if this plugin has no migrations. | |
408 def latest_migration | |
409 migrations.last | |
410 end | |
411 | |
412 # Returns the version numbers of all migrations for this plugin. | |
413 def migrations | |
414 migrations = Dir[migration_directory+"/*.rb"] | |
415 migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort | |
416 end | |
417 | |
418 # Migrate this plugin to the given version | |
419 def migrate(version = nil) | |
420 puts "Migrating #{id} (#{name})..." | |
421 Redmine::Plugin::Migrator.migrate_plugin(self, version) | |
422 end | |
423 | |
424 # Migrates all plugins or a single plugin to a given version | |
425 # Exemples: | |
426 # Plugin.migrate | |
427 # Plugin.migrate('sample_plugin') | |
428 # Plugin.migrate('sample_plugin', 1) | |
429 # | |
430 def self.migrate(name=nil, version=nil) | |
431 if name.present? | |
432 find(name).migrate(version) | |
433 else | |
434 all.each do |plugin| | |
435 plugin.migrate | |
436 end | |
437 end | |
438 end | |
439 | |
440 class Migrator < ActiveRecord::Migrator | |
441 # We need to be able to set the 'current' plugin being migrated. | |
442 cattr_accessor :current_plugin | |
443 | |
444 class << self | |
445 # Runs the migrations from a plugin, up (or down) to the version given | |
446 def migrate_plugin(plugin, version) | |
447 self.current_plugin = plugin | |
448 return if current_version(plugin) == version | |
449 migrate(plugin.migration_directory, version) | |
450 end | |
451 | |
452 def current_version(plugin=current_plugin) | |
453 # Delete migrations that don't match .. to_i will work because the number comes first | |
454 ::ActiveRecord::Base.connection.select_values( | |
455 "SELECT version FROM #{schema_migrations_table_name}" | |
456 ).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0 | |
457 end | |
458 end | |
459 | |
460 def migrated | |
461 sm_table = self.class.schema_migrations_table_name | |
462 ::ActiveRecord::Base.connection.select_values( | |
463 "SELECT version FROM #{sm_table}" | |
464 ).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort | |
465 end | |
466 | |
467 def record_version_state_after_migrating(version) | |
468 super(version.to_s + "-" + current_plugin.id.to_s) | |
469 end | |
470 end | |
277 end | 471 end |
278 end | 472 end |