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