3

最近给公司的 redmine 写了个提高 issue 流程的自动化程度插件。

redmine 官网上的开发者文档并不多,即使看完后,许多时候你也不知道接下来需要怎么做。好在 redmine 是用“约定高于配置”的 Rails 开发的,其命名相对规范。有必要时,可以猜测下对应的接口叫什么名字。实在摸不到门,阅读 redmine 和网上开源出来的 redmine 插件代码也是条路。

为了节省有同样需求的同行的时间,拯救我们日益凋亡的脑细胞,我在这里特意整理了开发过程中摸索出来的一些小技巧。

redmine 插件目录结构

.
├── app
│   ├── controllers
│   ├── models
│   └── views
│       └── settings
├── assets
│   └── javascripts
├── config
│   └── routes.rb
├── init.rb
├── lib
│   └── $plugin

其中 init.rb 是插件的入口。插件的逻辑主要放到 lib/$plugin 里面。其中 $plugin 应该是你的插件的名字。app 目录功能跟 Rails 的同名目录一样。比如你想在 redmine 中引入新的模型,或者加个路由,可以分别到 app/modelsapp/controllers 添加。不过别忘了修改 config/routes.rb 中的规则。要加个邮件模板,也是到 app/views 下加。至于 assets 里的资源文件,会在应用启动时拷贝到 public/plugin_assets/$plugin 路径下。

redmine 主要模型

Rails 数据库表有两种,一种命名是模型名的复数;另一种是 模型A复数_模型B复数,存储两个模型间的关系。

所以当我们列出数据库上的表时,我们便得到一份 redmine 模型清单。

 public | attachments                         | table | redmine
 public | auth_sources                        | table | redmine
 public | boards                              | table | redmine
 public | changes                             | table | redmine
 public | changeset_parents                   | table | redmine
 public | changesets                          | table | redmine
 public | changesets_issues                   | table | redmine
 public | comments                            | table | redmine
 public | custom_field_enumerations           | table | redmine
 public | custom_fields                       | table | redmine
 public | custom_fields_projects              | table | redmine
 public | custom_fields_roles                 | table | redmine
 public | custom_fields_trackers              | table | redmine
 public | custom_values                       | table | redmine
 public | documents                           | table | redmine
 public | email_addresses                     | table | redmine
 public | enabled_modules                     | table | redmine
 public | enumerations                        | table | redmine
 public | groups_users                        | table | redmine
 public | import_items                        | table | redmine
 public | imports                             | table | redmine
 public | issue_categories                    | table | redmine
 public | issue_relations                     | table | redmine
 public | issue_statuses                      | table | redmine
 public | issues                              | table | redmine
 public | journal_details                     | table | redmine
 public | journals                            | table | redmine
 public | member_roles                        | table | redmine
 public | members                             | table | redmine
 public | messages                            | table | redmine
 public | news                                | table | redmine
 public | open_id_authentication_associations | table | redmine
 public | open_id_authentication_nonces       | table | redmine
 public | projects                            | table | redmine
 public | projects_trackers                   | table | redmine
 public | queries                             | table | redmine
 public | queries_roles                       | table | redmine
 public | repositories                        | table | redmine
 public | roles                               | table | redmine
 public | roles_managed_roles                 | table | redmine
 public | schema_migrations                   | table | redmine
 public | settings                            | table | redmine
 public | time_entries                        | table | redmine
 public | tokens                              | table | redmine
 public | trackers                            | table | redmine
 public | user_preferences                    | table | redmine
 public | users                               | table | redmine
 public | versions                            | table | redmine
 public | watchers                            | table | redmine
 public | wiki_content_versions               | table | redmine
 public | wiki_contents                       | table | redmine
 public | wiki_pages                          | table | redmine
 public | wiki_redirects                      | table | redmine
 public | wikis                               | table | redmine

许多模型名都可以见名知意,比如 issue 对应中文界面上的 问题、tracker 对应中文界面上的 追踪。
不过有些命名和其中文译名差别不小,比如 journal 对应的是问题页面下的 说明、custom_field 对应的是 自定义属性。
这只能多在 rails consolepry 里试试看了。

hooks 和 patch

开发 redmine 插件基本靠两组 API,一个是 hook,另一个是 patch。
前者可以让你在 controller/view 特定的阶段插入自己的逻辑;后者可以覆盖掉原有的逻辑,抑或给 model 特定的阶段插入

对应的官方文档:
hook:http://www.redmine.org/projec...
patch:http://www.redmine.org/projec...

我就补充下文档没有的。

这两者需要在 init.rb 里完成初始化:

...
require '$plugin/hooks' # 引入 `lib/$plugin/hooks.rb`,让 hooks 生效

...
Rails.application.config.to_prepare do
  # 打上 Monkey Patch
  unless Issue.include?(AutoLicense::IssuePatch)
    Issue.send(:include, AutoLicense::IssuePatch)
  end
  ...

patch 主要的应用场景是覆盖掉 model 的 after_save 回调,触发自己的自动化逻辑。

    def self.included(base)
      base.send(:include, InstanceMethods)
      base.class_eval do
        after_save :after_save_trigger
      end
    end

    module InstanceMethods
      def after_save_trigger
        p self # 这里的 self 是具体的 model 实例

当然覆盖掉其他 Rails callback,比如 xx_create/xx_destroy,也是挺有用的。

hook 主要的应用场景是在 view 或 controller 执行流程中嵌入自己的逻辑。比如在特定 controller action 中加代码:

  class PluginControllerHookListener < Redmine::Hook::Listener
    def controller_issues_new_before_save(context)
      p context[:request].host_with_port
      # context 里面可以拿到 controller 上下文相关的变量,例如 :params
    end

还可以在特定的 view 里加代码:

  class PluginViewHookListener < Redmine::Hook::ViewListener
    def view_issues_new_top(context)
      js = <<-EOF
      console.log('Hello World')
      EOF
      "<script type=\"text/javascript\">#{javascript_cdata_section(js)}</script>"

当然如果想自定义前端代码,除了以字符串的形式插入,还可以直接引入整个资源文件。前面说到,

至于 assets 里的资源文件,会在应用启动时拷贝到 public/plugin_assets/$plugin 路径下。

所以把想要引入的资源文件放到 assets 目录下,在插入字符串中以 plugin_assets/$plugin/your_asset_file 路径引用即可。

顺便说一下,redmine 提供了一套 RESTful API:http://www.redmine.org/projec...
注意调用该接口需要提供用户的 API 调用键,细节参见文档。

redmine plugin setting

真正足够复杂的插件都需要一个配置界面 —— John Doe

redmine 给每个插件分配了一个配置页面,在 管理>插件>配置 里面,其对应的路由是 /settings/plugin/$plugin
插件开发者需要提供一个表单,通过这个表单,管理员能够给插件的一些配置项赋值。

这么做仅需两步:
第一步,需要准备一个表单模板,命名为 app/views/settings/_$plugin.html.erb

<table>
  <tbody>
    <tr>
        <td> 配置1 </td>
        <td>
          <input type="text" id="settings_field1"
            value="<%= settings['field1'] %>"
            name="settings[field1]" >
        </td>
    </tr>
    ...

第二步,在插件代码中访问配置值:

settings = Setting.plugin_$plugin
p settings['field1']

当然,如果插件所需的配置项不是简单的 KV 结构,你可以选择自己实现一套独立的配置机制。

通过修改 config/routes.rbapp/controllers 里面的内容,redmine 插件可以添加属于自己的路由:

RedmineApp::Application.routes.draw do
  get '/xxx', :to => 'plugin#xxx'
end

然后你可以在 app/models 放置自己的 model,用它来存储配置值。这就跟开发独立的 Rails 应用差不多。


spacewander
5.6k 声望1.5k 粉丝

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.


引用和评论

0 条评论