Redmine插件开发

Redmine原生系统并不能满足我们所有需求, Rails框架具有插件扩展机制。通过插件,我们可以对原系统的功能进行扩展和修改。

学习资料:

《Plugin Tutorial》 http://www.redmine.org/projects/redmine/wiki/Plugin_Tutorial
《Plugin Internals》 http://www.redmine.org/projects/redmine/wiki/Plugin_Internals
《Redmine Plugin Hooks》http://www.redmine.org/projects/redmine/wiki/Hooks
《Redmine plugin hooks list》http://www.redmine.org/projects/redmine/wiki/Hooks_List

上边四篇文章来源于Redmine官网。涵盖了插件开发的各个方面,简洁而全面。下面的内容算是上述文章的读书笔记,纯属画蛇添足。

一、 新建一个简单的插件

  1. 设置环境变量。 Linux环境下,执行export RAILS_ENV=”production”。
  2. 新建插件框架代码。

命令: ruby script/rails generate redmine_plugin xxx, 其中xxx为插件名。

命令执行后, 终端会打印出新建的文件列表。

# plugins/xxx为插件的目录
#新建的文件结构和redmine核心文件的结构基本一致
create  plugins/xxx/app 
create  plugins/xxx/app/controllers
#新增的控制器放置于此
create  plugins/xxx/app/helpers
#新增的helper放置于此
create  plugins/xxx/app/models
#新增的模型 
create  plugins/xxx/app/views
#新增的视图
create  plugins/xxx/db/migrate
#新增的数据库迁移文件
create  plugins/xxx/lib/tasks
#新增的rake任务
create  plugins/xxx/assets/images
#图片静态资源
create  plugins/xxx/assets/javascripts
#前端js脚本
create  plugins/xxx/assets/stylesheets
#前端css样式表
create  plugins/xxx/test
#测试相关
create  plugins/xxx/test/fixtures
create  plugins/xxx/test/unit
create  plugins/xxx/test/functional
create  plugins/xxx/test/integration
create  plugins/xxx/test/test_helper.rb
create  plugins/xxx/README.rdoc
#说明文档
create  plugins/xxx/init.rb
#初始化文档: 配置访问权限, 记录插件的基本说明信息, 设定嵌入redmine的按钮 
create  plugins/xxx/config/routes.rb
# 新增的路由
create  plugins/xxx/config/locales
#语言翻译文件
create  plugins/xxx/config/locales/en.yml
#语言翻译文件: 英语翻译文件

3.编辑init.rb文件

默认生成下方的文档。 插件开发者需要填入插件名称name, 作者名称author, 插件描述description, 插件版本version, 插件相关页面url, 作者相关页面author_url。

Redmine::Plugin.register :xxx do
    name 'Xxx plugin'
    author 'Author name'
    description 'This is a plugin for Redmine'
    version '0.0.1'
    url 'http://example.com/path/to/plugin'
    author_url 'http://example.com/about'
end

至此, 一个新插件就建立完毕。重启服务器, 进入系统管理页面。 点击插件按钮, 系统安装的全部插件信息会列出。

bar_of_pluginss

刚刚建立的插件也在列表中, 我们可以浏览到init.rb文件中编辑的插件名, 作者名等信息。

plugins_list

 二、插件的功用

第一节中, 我们仅生成了插件的框架, 并没有任何实际的功能。通过插件,能够办到哪些事情?从代码功能的角度看,插件能够办到以下几类事情。

  1. 添加
  • 当我们计划开发全新的模块时, 往往需要构造全新的控制器、视图以及模型等待。通过插件能够做到这些,也就意味着,通过插件能够开发出Rails框架允许的任意新功能。
  • 我们为已存在的类添加新的方法。
  1. 覆盖
  • 对于已有视图, 插件可以通过简易的方法十分轻易地覆盖掉核心视图文件的内容。
  • 包裹已有的方法,令原有的方法有更多扩展的含义。
  1. 扩展
  • 通过系统自带的钩子, 向系统的视图、模型、控制器的钩子所在位置注入新的代码, 扩展原有逻辑
  • 通过简单的配置,在系统的菜单中添加新的标签或是按钮。

在实际的开发中, 我们通常会混合使用上述功能, 从而开发出五花八门的插件。 下面将展开介绍上述功能。

2.1 添加控制器/模型/视图/Helper/数据库迁移文件/路由文件/Rake任务

如果要建立新的模型、控制器、视图、Helper、数据库迁移、路由、Rake任务等等, 在插件的相应文件夹中,按照与开发redmine核心模块相同的方法,直接建立文件编辑即可。这部分内容本无需多言,由于系统提供了命令,协助我们自动建立部分文件,下面就简述之。

1. 建立模型

命令: ruby script/rails generate redmine_plugin_model xxx yyy [field[:type][:index] field[:type][:index] ...]

其中, xxx为插件名, yyy为要建立的模型名。命令执行后,模型文件和数据库迁移文件会被创建出来。 我们可以根据需要在此基础上修改代码。

2. 建立控制器

命令: ruby script/rails generate redmine_plugin_controller xxx yyy action_name

其中, xxx为插件名, yyy为要建立的控制器名。 命令执行后, 控制器文件, 以及相应的Helper文件、视图文件都会一并被创建出来。 我们可根据需要在此基础上修改代码。

 2.2 已有类添加方法

编写插件时, 有时我们会用到Redmine核心文件中的类,但已有的方法又无法完全满足需求。这样我们希望在不修改核心代码的前提下, 为已经存在的类添加新的方法。 Redmine插件可以做到这点。以向类Mailer中新建类方法xxxxxx, 和实例方法yyyyyy为例。

步骤一:在lib文件下新建文件xxx/mailer_patch.rb。 其中,xxx为插件的名称,它是笔者写插件的习惯规则。由于所有插件的lib文件夹下的文件路径不得冲突,有了xxx目录的区分, 路径冲突的问题就几乎不存在了。

步骤二: 编辑mailer_patch。添加如下内容

#encoding: utf-8
require_dependency 'mailer'

module xxxxModule
    module MailerPatch
        def self.included(base)
            base.send(:include, InstanceMethods)
            base.class_eval do  
                def self.update_chengguowu_mails(options={})
                    …………
                end           
            end
        end
         
        module InstanceMethods
            def send_update_chengguowu_mails()
                …………
            end
        end
    end
end

Mailer.send(:include, xxxxModule::MailerPatch)

上述代码将类方法update_chengguowu_mails和实例方法send_update_chengguowu_mails加入到了已有类Mailer中。

  • #encoding: utf-8如果代码中有中文, 需要在文件的开头加这一句
  • require_dependency ‘mailer’因为我们要添加方法的类在rb文件中, 因此在文件的开头, 要将相应文件引入。
  • module xxxModule包裹在最外层的Module, 作用相当于名字空间, 确保插件之间的模块不会冲突。 xxx是插件名, 而紧随其后的Module是为了使得xxxModule不会与插件中名为xxx的类名称冲突(插件中的模型类很有可能被命名为xxx), 这样的规定是笔者自己的风格,不是强制的。
  • class_eval do …… end之间加入的代码是添加到类中的声明或类方法。
  • module InstanceMethods …… end 之间加入的代码是添加到类中的实例方法。
  • send(:include, xxxxModule::MailerPatch)这句代码将封装在上方模块中的方法混入了Mailer类中

步骤三: 在init.rb文件中, 加入下方两句代码. 第一句引入redmine的核心代码, 第二句引入刚刚在./lib文件夹下新建的文件。

require 'redmine'
require_dependency 'xxx/mailer_patch'

重启服务器, 插件生效。

2.3 覆盖已有视图

Redmine系统核心代码中有很多视图文件。 如果我们想修改它们, 最直接的方法便是通过插件覆盖原有视图。

例如, 核心代码中, 显示问题票页面的视图路径是./app/views/issues/show.html.erb。如果要覆盖这个视图, 我们只需在某插件xxx的文件夹中, 建立文件./plugins/xxx/ app/views/issues/show.html.erb, 那么系统便会用插件中的视图来展示问题票页面。如果多个插件都定义了同一个路径下的视图(issues/show.html.erb),那么系统最后加载的插件的视图生效。除非万不得已, 笔者十分不推荐这种做法。 因为多个插件之间的同一视图,只能有一个生效。

 2.4 包裹已有方法

插件可以覆盖Redmine的核心视图文件, 却无奈何模型文件、控制器文件。如何覆盖文件中的类或模块? 覆插整个类或模块插件是做不到的, 但可以通过某种方法扩展已有类/模块的方法。

以扩展ProjectsHelper模块的project_settings_tabs方法为例

步骤一:在lib文件下新建文件xxx/projects_helper_patch.rb。 其中,xxx为插件的名称,它是笔者写插件的习惯规则。由于所有插件的lib文件夹下的文件路径不得冲突,有了xxx目录的区分, 路径冲突的问题就几乎不存在了。

步骤二: 编辑文件xxx/projects_helper_patch.rb。

require_dependency 'projects_helper'
module RedmineTimingTaskModule
    module ProjectsHelperPatch
        def self.included(base)
            base.send(:include, InstanceMethods)
            base.class_eval do
              #unloadable
              alias_method_chain :project_settings_tabs, :redmine_timing_task
            end    
        end
        module InstanceMethods
            def  project_settings_tabs_with_redmine_timing_task
                tabs = project_settings_tabs_without_redmine_timing_task
                action = {:name => 'timing_task', :controller => :redmine_timing_task, :action => :index, :partial => 'redmine_timing_task/setting/timing_tab', :label => :timing_task_tab}
                tabs << action 
                tabs        
            end
        end
    end
end 

ProjectsHelper.send(:include, MRedmineTimingTask::ProjectsHelperPatch)

上述代码将ProjectsHelperPatch模块的方法用新方法project_settings_tabs_with_redmine_timing_task包裹起来,使得project_settings_tabs有了更多内涵。

  • require_dependency ‘projects_helper’引入我们所修改的类/模块所在文件
  • alias_method_chain :project_settings_tabs, :redmine_timing_task, 在类/模块中用alias_method_chain声明后, 系统调用project_settings_tabs方法时, 实际会调用到我们随后定义的project_settings_tabs_with_redmine_timing_task方法, 而project_settings_tabs_without_redmine_timing_task则相当于原始的project_settings_tabs方法。 在project_settings_tabs_with_redmine_timing_task方法的实现中,写入project_settings_tabs_without_redmine_timing_tas,则实现了对原方法的包裹。

 

步骤三: 在init.rb文件中, 加入下方两句代码。 第一句引入redmine的核心代码, 第二句引入刚刚在./lib文件夹下新建的文件。

require 'redmine'
require_dependency 'xxx/projects_helper_patch'

注意: 官方文档中有这么一段话, It is important to note that this kind of wrapping can only be done once per method. In the case of multiple plugins using this trick, then only the last evaluation of the alias_method_chain would be valid and all the previous ones would be ignored. 我对这句话的理解是, 某个方法只能被插件包裹一次。如果多个插件包裹了同一方法,那么只有最后加载的插件的包裹有效。 但是在实践中, 同一方法被包裹了若干次后, 所有添加的内容都有效, 到底是官方文档有误,还是我理解错误,有待继续了解。

2.5 钩子注入代码

Redmine核心代码中预留了很多“钩子”。视图中存在大量的钩子。插件可以将新的代码注入到钩子所在的位置。下面就简述下通过钩子注入代码的方法。

步骤一: 首先需要查看系统有哪些钩子。打开终端,进入到系统目录./app下,执行命令:  grep –r call_hook, 便能看到诸多钩子信息。 根据所在的文件位置, 选择出你需要的钩子。

这里,我们选择一个视图钩子和一个控制器钩子。

view_issues_bulk_edit_details_bottom
controller_issues_bulk_edit_before_save

步骤二

插件xxx的lib文件夹下编辑文件./plugins/xxx/lib/xxx/xxxHookListener.rb,编辑文件内容:

module XXXModule
    class XXXHookListener < Redmine::Hook::ViewListener
        def view_issues_bulk_edit_details_bottom(context={})
            context[:controller].send(:render_to_string, {
                :partial => "hook/xxx /view_issues_bulk_edit_details_bottom",
                :locals => context
            })
        end
        def controller_issues_bulk_edit_before_save(context={})
            Code Area A……
        end
    end
end

上述代码, 将位于./plugins/xxx/app/view/hook/_view_issues_bulk_edit_details_bottom.html.erb的视图文件渲染到了view_issues_bulk_edit_details_bottom钩子所在的位置。同时也将”Code Area A”部分的代码添加到了钩子controller_issues_bulk_edit_before_save所在的位置。

步骤三:编辑插件的初始化文件: ./plugins/xxx/init.rb, 加入两句代码。

require 'redmine'
require_dependency ' xxx/xxxHookListener.rb '

重启redmine服务器, 插件生效。

2.6 配置菜单按钮

官方文档中对如何添加系统的菜单按钮有详细描述, 方法很简单,就是编辑./plugins/xxx/init.rb文件。

Redmine::Plugin.register :redmine_updater do
    project_module :updater do
        permission :updater, :updater => :index, :public => true
    end
    menu :project_menu, :updater, { :controller => 'updater', :action => 'index' }, :caption => :label_updater, :before => :settings, :param => :project_id
end

其中关键的方法是:

menu(menu_name, item_name, url, options={})

详情请参考 http://www.redmine.org/projects/redmine/wiki/Plugin_Tutorial 中的Extending Menus部分。

翻译文件

Redmine中,通过翻译文件,系统可以根据用户的语言设置,显示不同国家的文字。翻译文件位于./plugins/xxx/config/locales/路径下。我们通常用到的翻译文件为em.yml,zh.yml, 前者为英文翻译,后者为汉语翻译。翻译文件的内容均是一个个的键值对。键作为对翻译文件的引用,值作为显示在页面上的内容。插件的下面看一个简单的例子(针对上边的init.rb文件):

zn:
  […]
  label_updater: “批量更新文件”
  permission_updater: "批量文件更新"
  project_module_updater: "批量文件更新"
  • label_updater: “批量更新文件”, 这句翻译对应于menu定义中的:caption => :label_updater, 按钮上显示的文件将会是“批量更新文件”
  • permission_updater: 批量更新文件”, 此句翻译对应于permission :updater, :updater => :index, :public => true,在管理员配置角色权限的页面中,相应权限的名称由此翻译条目指定。

trans1

  • project_module_updater: “批量文件更新”, 这句翻译对应于project_module :updater do … end。在管理员配置角色权限的页面中,相应的权限分组名称由此翻译条目指定。

trans2

2.7 插件开发规范

redmine插件的开发中, 笔者发现错误的插件开发方式会使系统部分功能莫名失效, 故而列出几个会使引发错误的点。

  1. 插件中同路径的视图会覆盖核心代码中的视图;插件之间同路径的视图也会相互覆盖。可通过下述规范解决此问题。
  • 插件要尽量使用核心视图的钩子来修改核心代码中的视图。 如果无奈必须使用, 一定要做到心中有数。
  • 插件自定义的视图, 一定要放在特定的文件夹中。 例如xxx插件自定义的视图就可以放在./plugins/xxx/app/views/xxx路径下, xxx可以起到保证插件视图之间的路径不冲突的作用。
  • 可以开发出冲突检验脚本, 判断核心代码视图以及插件之间的视图是否存在冲突。
  1. plugins/xxx/app/controllers, plugins/xxx/app/models路径下的类也要注意保证文件路径唯一,命名唯一。
  2. Plugins/xxx/lib/路径下的文件命名必须唯一, 模块/类的命名也要唯一。
  • 文件可以放在路径plugins/xxx/lib/xxx下, xxx可确保文件路径唯一
  • 模块/类在具体的实现中, 均可以用模块xxxModule进行外层的包裹, 它相当于名字空间, 确保插件新添加的类/模块不会与其他类/模块冲突。
  1. 需要保证翻译文件中的键名不会与已有键名冲突。