Class: Redmine::Plugin

Inherits:
Object
  • Object
show all
Defined in:
lib/redmine/plugin.rb

Overview

Base class for Redmine plugins. Plugins are registered using the register class method that acts as the public constructor.

Redmine::Plugin.register :example do
  name 'Example plugin'
  author 'John Smith'
  description 'This is an example plugin for Redmine'
  version '0.0.1'
  settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
end

Plugin attributes

settings is an optional attribute that let the plugin be configurable. It must be a hash with the following keys:

  • :default: default value for the plugin settings

  • :partial: path of the configuration partial view, relative to the plugin app/views directory

Example:

settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'

In this example, the settings partial will be found here in the plugin directory: app/views/settings/_settings.rhtml.

When rendered, the plugin settings value is available as the local variable settings

See: www.redmine.org/projects/redmine/wiki/Plugin_Tutorial

Defined Under Namespace

Classes: MigrationContext, Migrator

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id) ⇒ Plugin

Returns a new instance of Plugin.


194
195
196
# File 'lib/redmine/plugin.rb', line 194

def initialize(id)
  @id = id.to_sym
end

Class Attribute Details

.registered_pluginsObject (readonly)

Returns the value of attribute registered_plugins


65
66
67
# File 'lib/redmine/plugin.rb', line 65

def registered_plugins
  @registered_plugins
end

Instance Attribute Details

#idObject (readonly)

Returns the value of attribute id


79
80
81
# File 'lib/redmine/plugin.rb', line 79

def id
  @id
end

Class Method Details

.allObject

Returns an array of all registered plugins


148
149
150
# File 'lib/redmine/plugin.rb', line 148

def self.all
  registered_plugins.values.sort
end

.clearObject

Clears the registered plugins hash It doesn't unload installed plugins


160
161
162
# File 'lib/redmine/plugin.rb', line 160

def self.clear
  @registered_plugins = {}
end

.def_field(*names) ⇒ Object


68
69
70
71
72
73
74
75
76
# File 'lib/redmine/plugin.rb', line 68

def def_field(*names)
  class_eval do
    names.each do |name|
      define_method(name) do |*args|
        args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
      end
    end
  end
end

.find(id) ⇒ Object

Finds a plugin by its id Returns a PluginNotFound exception if the plugin doesn't exist


154
155
156
# File 'lib/redmine/plugin.rb', line 154

def self.find(id)
  registered_plugins[id.to_sym] || raise(PluginNotFound)
end

.installed?(id) ⇒ Boolean

Checks if a plugin is installed

Parameters:

  • id (String)

    name of the plugin

Returns:

  • (Boolean)

173
174
175
# File 'lib/redmine/plugin.rb', line 173

def self.installed?(id)
  registered_plugins[id.to_sym].present?
end

.loadObject


177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/redmine/plugin.rb', line 177

def self.load
  Dir.glob(File.join(self.directory, '*')).sort.each do |directory|
    if File.directory?(directory)
      lib = File.join(directory, "lib")
      if File.directory?(lib)
        $:.unshift lib
        ActiveSupport::Dependencies.autoload_paths += [lib]
      end
      initializer = File.join(directory, "init.rb")
      if File.file?(initializer)
        require initializer
      end
    end
  end
  Redmine::Hook.call_hook :after_plugins_loaded
end

.migrate(name = nil, version = nil) ⇒ Object

Migrates all plugins or a single plugin to a given version Exemples:

Plugin.migrate
Plugin.migrate('sample_plugin')
Plugin.migrate('sample_plugin', 1)

524
525
526
527
528
529
530
531
532
# File 'lib/redmine/plugin.rb', line 524

def self.migrate(name=nil, version=nil)
  if name.present?
    find(name).migrate(version)
  else
    all.each do |plugin|
      plugin.migrate
    end
  end
end

.mirror_assets(name = nil) ⇒ Object

Mirrors assets from one or all plugins to public/plugin_assets


486
487
488
489
490
491
492
493
494
# File 'lib/redmine/plugin.rb', line 486

def self.mirror_assets(name=nil)
  if name.present?
    find(name).mirror_assets
  else
    all.each do |plugin|
      plugin.mirror_assets
    end
  end
end

.register(id, &block) ⇒ Object

Plugin constructor: instanciates a new Redmine::Plugin with given id and make it evaluate the given block

Example

Redmine::Plugin.register :example do
  name 'Example plugin'
  author 'John Smith'
  description 'This is an example plugin for Redmine'
  version '0.0.1'
  requires_redmine version_or_higher: '3.0.0'
end

92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/redmine/plugin.rb', line 92

def self.register(id, &block)
  p = new(id)
  p.instance_eval(&block)

  # Set a default name if it was not provided during registration
  p.name(id.to_s.humanize) if p.name.nil?
  # Set a default directory if it was not provided during registration
  p.directory(File.join(self.directory, id.to_s)) if p.directory.nil?

  unless File.directory?(p.directory)
    raise PluginNotFound, "Plugin not found. The directory for plugin #{p.id} should be #{p.directory}."
  end

  # Adds plugin locales if any
  # YAML translation files should be found under <plugin>/config/locales/
  Rails.application.config.i18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))

  # Prepends the app/views directory of the plugin to the view path
  view_path = File.join(p.directory, 'app', 'views')
  if File.directory?(view_path)
    ActionController::Base.prepend_view_path(view_path)
    ActionMailer::Base.prepend_view_path(view_path)
  end

  # Add the plugin directories to rails autoload paths
  engine_cfg = Rails::Engine::Configuration.new(p.directory)
  engine_cfg.paths.add 'lib', eager_load: true
  Rails.application.config.eager_load_paths += engine_cfg.eager_load_paths
  Rails.application.config.autoload_once_paths += engine_cfg.autoload_once_paths
  Rails.application.config.autoload_paths += engine_cfg.autoload_paths
  ActiveSupport::Dependencies.autoload_paths +=
    engine_cfg.eager_load_paths + engine_cfg.autoload_once_paths + engine_cfg.autoload_paths

  # Defines plugin setting if present
  if p.settings
    Setting.define_plugin_setting p
  end

  # Warn for potential settings[:partial] collisions
  if p.configurable?
    partial = p.settings[:partial]
    if @used_partials[partial]
      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."
      )
    end
    @used_partials[partial] = p.id
  end

  registered_plugins[id] = p
end

.unregister(id) ⇒ Object

Removes a plugin from the registered plugins It doesn't unload the plugin


166
167
168
# File 'lib/redmine/plugin.rb', line 166

def self.unregister(id)
  @registered_plugins.delete(id)
end

Instance Method Details

#<=>(plugin) ⇒ Object


211
212
213
# File 'lib/redmine/plugin.rb', line 211

def <=>(plugin)
  self.id.to_s <=> plugin.id.to_s
end

#activity_provider(*args) ⇒ Object

Registers an activity provider.

Options:

  • :class_name - one or more model(s) that provide these events (inferred from event_type by default)

  • :default - setting this option to false will make the events not displayed by default

A model can provide several activity event types.

Examples:

register :news
register :scrums, :class_name => 'Meeting'
register :issues, :class_name => ['Issue', 'Journal']

Retrieving events: Associated model(s) must implement the find_events class method. ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.

The following call should return all the scrum events visible by current user that occurred in the 5 last days:

Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only

Note that :view_scrums permission is required to view these events in the activity view.


418
419
420
# File 'lib/redmine/plugin.rb', line 418

def activity_provider(*args)
  Redmine::Activity.register(*args)
end

#assets_directoryObject

Returns the absolute path to the plugin assets directory


207
208
209
# File 'lib/redmine/plugin.rb', line 207

def assets_directory
  File.join(directory, 'assets')
end

#configurable?Boolean

Returns true if the plugin can be configured.

Returns:

  • (Boolean)

440
441
442
# File 'lib/redmine/plugin.rb', line 440

def configurable?
  settings && settings.is_a?(Hash) && !settings[:partial].blank?
end

#delete_menu_item(menu, item) ⇒ Object

Removes item from the given menu.


343
344
345
# File 'lib/redmine/plugin.rb', line 343

def delete_menu_item(menu, item)
  Redmine::MenuManager.map(menu).delete(item)
end

#latest_migrationObject

Returns the version number of the latest migration for this plugin. Returns nil if this plugin has no migrations.


503
504
505
# File 'lib/redmine/plugin.rb', line 503

def latest_migration
  migrations.last
end

Adds an item to the given menu. The id parameter (equals to the project id) is automatically added to the url.

menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'

name parameter can be: :top_menu, :account_menu, :application_menu or :project_menu


337
338
339
# File 'lib/redmine/plugin.rb', line 337

def menu(menu, item, url, options={})
  Redmine::MenuManager.map(menu).push(item, url, options)
end

#migrate(version = nil) ⇒ Object

Migrate this plugin to the given version


514
515
516
# File 'lib/redmine/plugin.rb', line 514

def migrate(version = nil)
  Redmine::Plugin::Migrator.migrate_plugin(self, version)
end

#migration_directoryObject

The directory containing this plugin's migrations (plugin/db/migrate)


497
498
499
# File 'lib/redmine/plugin.rb', line 497

def migration_directory
  File.join(directory, 'db', 'migrate')
end

#migrationsObject

Returns the version numbers of all migrations for this plugin.


508
509
510
511
# File 'lib/redmine/plugin.rb', line 508

def migrations
  migrations = Dir[migration_directory+"/*.rb"]
  migrations.map {|p| File.basename(p).match(/0*(\d+)\_/)[1].to_i}.sort
end

#mirror_assetsObject


444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/redmine/plugin.rb', line 444

def mirror_assets
  source = assets_directory
  destination = public_directory
  return unless File.directory?(source)

  source_files = Dir[source + "/**/*"]
  source_dirs = source_files.select {|d| File.directory?(d)}
  source_files -= source_dirs

  unless source_files.empty?
    base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
    begin
      FileUtils.mkdir_p(base_target_dir)
    rescue => e
      raise "Could not create directory #{base_target_dir}: " + e.message
    end
  end

  source_dirs.each do |dir|
    # strip down these paths so we have simple, relative paths we can
    # add to the destination
    target_dir = File.join(destination, dir.gsub(source, ''))
    begin
      FileUtils.mkdir_p(target_dir)
    rescue => e
      raise "Could not create directory #{target_dir}: " + e.message
    end
  end

  source_files.each do |file|
    begin
      target = File.join(destination, file.gsub(source, ''))
      unless File.exist?(target) && FileUtils.identical?(file, target)
        FileUtils.cp(file, target)
      end
    rescue => e
      raise "Could not copy #{file} to #{target}: " + e.message
    end
  end
end

#permission(name, actions, options = {}) ⇒ Object

Defines a permission called name for the given actions.

The actions argument is a hash with controllers as keys and actions as values (a single value or an array):

permission :destroy_contacts, { :contacts => :destroy }
permission :view_contacts, { :contacts => [:index, :show] }

The options argument is a hash that accept the following keys:

  • :public => the permission is public if set to true (implicitly given to any user)

  • :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member

  • :read => set it to true so that the permission is still granted on closed projects

Examples

# A permission that is implicitly given to any user
# This permission won't appear on the Roles & Permissions setup screen
permission :say_hello, { :example => :say_hello }, :public => true, :read => true

# A permission that can be given to any user
permission :say_hello, { :example => :say_hello }

# A permission that can be given to registered users only
permission :say_hello, { :example => :say_hello }, :require => :loggedin

# A permission that can be given to project members only
permission :say_hello, { :example => :say_hello }, :require => :member

371
372
373
374
375
376
377
378
379
380
381
# File 'lib/redmine/plugin.rb', line 371

def permission(name, actions, options = {})
  if @project_module
    Redmine::AccessControl.map do |map|
      map.project_module(@project_module) do |map|
        map.permission(name, actions, options)
      end
    end
  else
    Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
  end
end

#project_module(name, &block) ⇒ Object

Defines a project module, that can be enabled/disabled for each project. Permissions defined inside block will be bind to the module.

project_module :things do
  permission :view_contacts, { :contacts => [:list, :show] }, :public => true
  permission :destroy_contacts, { :contacts => :destroy }
end

390
391
392
393
394
# File 'lib/redmine/plugin.rb', line 390

def project_module(name, &block)
  @project_module = name
  self.instance_eval(&block)
  @project_module = nil
end

#public_directoryObject


198
199
200
# File 'lib/redmine/plugin.rb', line 198

def public_directory
  File.join(self.class.public_directory, id.to_s)
end

#requires_redmine(arg) ⇒ Object

Sets a requirement on Redmine version Raises a PluginRequirementError exception if the requirement is not met

Examples

# Requires Redmine 0.7.3 or higher
requires_redmine :version_or_higher => '0.7.3'
requires_redmine '0.7.3'

# Requires Redmine 0.7.x or higher
requires_redmine '0.7'

# Requires a specific Redmine version
requires_redmine :version => '0.7.3'              # 0.7.3 only
requires_redmine :version => '0.7'                # 0.7.x
requires_redmine :version => ['0.7.3', '0.8.0']   # 0.7.3 or 0.8.0

# Requires a Redmine version within a range
requires_redmine :version => '0.7.3'..'0.9.1'     # >= 0.7.3 and <= 0.9.1
requires_redmine :version => '0.7'..'0.9'         # >= 0.7.x and <= 0.9.x

234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/redmine/plugin.rb', line 234

def requires_redmine(arg)
  arg = {:version_or_higher => arg} unless arg.is_a?(Hash)
  arg.assert_valid_keys(:version, :version_or_higher)

  current = Redmine::VERSION.to_a
  arg.each do |k, req|
    case k
    when :version_or_higher
      unless req.is_a?(String)
        raise ArgumentError.new(":version_or_higher accepts a version string only")
      end

      unless compare_versions(req, current) <= 0
        raise PluginRequirementError.new(
          "#{id} plugin requires Redmine #{req} or higher " \
          "but current is #{current.join('.')}"
        )
      end
    when :version
      req = [req] if req.is_a?(String)
      if req.is_a?(Array)
        unless req.detect {|ver| compare_versions(ver, current) == 0}
          raise PluginRequirementError.new(
            "#{id} plugin requires one the following Redmine versions: " \
            "#{req.join(', ')} but current is #{current.join('.')}"
          )
        end
      elsif req.is_a?(Range)
        unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0
          raise PluginRequirementError.new(
            "#{id} plugin requires a Redmine version between #{req.first} " \
            "and #{req.last} but current is #{current.join('.')}"
          )
        end
      else
        raise ArgumentError.new(":version option accepts a version string, an array or a range of versions")
      end
    end
  end
  true
end

#requires_redmine_plugin(plugin_name, arg) ⇒ Object

Sets a requirement on a Redmine plugin version Raises a PluginRequirementError exception if the requirement is not met

Examples

# Requires a plugin named :foo version 0.7.3 or higher
requires_redmine_plugin :foo, :version_or_higher => '0.7.3'
requires_redmine_plugin :foo, '0.7.3'

# Requires a specific version of a Redmine plugin
requires_redmine_plugin :foo, :version => '0.7.3'              # 0.7.3 only
requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0']   # 0.7.3 or 0.8.0

293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/redmine/plugin.rb', line 293

def requires_redmine_plugin(plugin_name, arg)
  arg = {:version_or_higher => arg} unless arg.is_a?(Hash)
  arg.assert_valid_keys(:version, :version_or_higher)

  begin
    plugin = Plugin.find(plugin_name)
  rescue PluginNotFound
    raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin")
  end
  current = plugin.version.split('.').collect(&:to_i)

  arg.each do |k, v|
    v = [] << v unless v.is_a?(Array)
    versions = v.collect {|s| s.split('.').collect(&:to_i)}
    case k
    when :version_or_higher
      unless versions.size == 1
        raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)")
      end

      unless (current <=> versions.first) >= 0
        raise PluginRequirementError.new(
          "#{id} plugin requires the #{plugin_name} plugin #{v} or higher " \
          "but current is #{current.join('.')}"
        )
      end
    when :version
      unless versions.include?(current.slice(0, 3))
        raise PluginRequirementError.new(
          "#{id} plugin requires one the following versions of #{plugin_name}: " \
          "#{v.join(', ')} but current is #{current.join('.')}"
        )
      end
    end
  end
  true
end

#to_paramObject


202
203
204
# File 'lib/redmine/plugin.rb', line 202

def to_param
  id
end

#wiki_format_provider(name, *args) ⇒ Object

Registers a wiki formatter.

Parameters:

  • name - formatter name

  • formatter - formatter class, which should have an instance method to_html

  • helper - helper module, which will be included by wiki pages (optional)

  • html_parser class reponsible for converting HTML to wiki text (optional)

  • options - a Hash of options (optional)

    • :label - label for the formatter displayed in application settings

Examples:

wiki_format_provider(:custom_formatter, CustomFormatter, :label => "My custom formatter")

435
436
437
# File 'lib/redmine/plugin.rb', line 435

def wiki_format_provider(name, *args)
  Redmine::WikiFormatting.register(name, *args)
end