Redmine. Как писать плагины

в 7:42, , рубрики: rails 3, redmine, ruby, ruby on rails, Блог компании Монастырёв и Ко, плагины, метки: , , ,

В своем прошлом посте я постарался достаточно детально описать все тонкости установки Redmine на Linux Ubuntu. В этом, хочу рассказать о тонкостях написания плагинов под Redmine, об основных возможностях изменения функциональности стандартного Redmine, о подводных камнях, которые встречались моей команде на этом пути.

Думаю, эта статья будет полезна тем, кто уже знаком с основами фреймворка Ruby on Rails и хочет начать разрабатывать плагины для Redmine.

Прежде всего, стоит разделить все плагины Redmine на две категории:

В первую попадают те плагины, которые фактически не затрагивают функциональность стандартного Redmine. По сути это обычные Rails-приложения внутри Redmine, с ними возникает мало сложностей, поэтому они малоинтересны. На официальном сайте Redmine есть неплохой туториал, подробно описывающий как создать плагин для голосования.

Все немного сложнее, когда плагин должен изменять встроенную функциональность!

Начнем с команды, которая создает структуру папок для плагина Redmine. Пусть наш плагин будет называться Luxury Buttons. Перейдем в корневую папку Redmine, запустим команду, создающую структуру папок:

$cd /usr/share/srv-redmine/redmine-2.3
$rails generate redmine_plugin LuxuryButtons

После выполнения команды в папке plugins должна появиться папка luxury_buttons со следующей структурой:
Redmine. Как писать плагины
В папку lib стоит сразу добавить, папку, совпадающую с названием плагина, т.е. папку luxury_buttons (далее папка патчинга). В этой папке, в дальнейшем, будут лежать файлы патчинга различных методов Redmine.

Почему мы назвали эту папку также как назвали плагин? Это просто рекомендация, папку можно назвать и по-другому, но тут возникает первый подводный камень: если в другом плагине название этой папки будет совпадать, и будет совпадать название файла патчинга, то один из файлов патчинга просто не примениться! Поэтому, я рекомендую, называть папку патчинга одноименно с названием плагина. Такой способ минимизирует возникновение ошибок!

Когда плагин должен что-то добавить во вьюшке.

Допустим нам нужно что-то добавить в стандартную вьюшку Redmine. Самый простой и самый неправильный способ сделать это – переписать вьюшку внутри плагина. Обычно это делается путем копирования файла-вьюшки из ядра Redmine в соответствующую директорию плагина и дальнейшим редактированием этого файла. Вот, например, в одном из наших плагинов, мы переписываем вьюшку с формой сохранения запроса.

Redmine. Как писать плагины

Почему так делать плохо:

  • Вы обрекаете себя на постоянный мониторинг актуальности вашей вьюшки. Если в новой версии Redmine в данной вьюшке что-то поменяется, то вы потеряете эту функциональность. Отслеживать актуальность вьюшки довольно сложно.
  • Если появится другой плагин, который перепишет эту же вьюшку, то применится либо ваша вьюшка, либо вьюшка другого плагина. Какая вьюшка применится, зависит от очередности плагинов.

Поэтому, лучше использовать альтернативные методы.

Хуки

Хук во вьюшке – это такая строчка кода, которая позволяет встроить во вьюшку свое содержимое. Чтобы найти хук, нужно просто выполнить поиск подстроки «hook» по всем файлам Redmine или можно воспользоваться вот этой табличкой.

Подключение хука

Мы стараемся хранить все подключения хуков вьюшек в одном файле. Этот файл нужно подключить в init.rb вот так:

require 'luxury_buttons/view_hooks'

Содержимое самого файла может быть таким:

module LuxuryButtons
  module LuxuryButtons
    class Hooks  < Redmine::Hook::ViewListener
      render_on( :view_issues_form_details_top, :partial => 'lu_buttons/view_issues_form_details_top')
      render_on( :view_layouts_base_html_head, :partial => 'lu_buttons/page_header')
      render_on( :view_issues_show_description_bottom, :partial => "lu_buttons/button_bar" )
      render_on( :view_issues_history_journal_bottom, :partial => "lu_buttons/journal_detail")
    end
  end
end

Название первого модуля должно совпадать с названием плагина, второго – с названием папки патчинга.

Внутри класса находятся функции, которые показывают, в какой хук какой шаблон нужно отрендерить.

Если два плагина будут использовать один и тот же хук, то во вьюшки появиться содержимое и из первого и из второго плагина. Т.е. хуки не переписывают друг друга.

С хуками возникает две проблемы:

  • Хука может не быть.
  • Иногда нужно удалить что-то из вьюшки, а хук позволяет только добавить.

Единственный способ, который мы нашли, что бы решать данные проблемы – это использование JQuery для модификации содержимого на странице, когда вьюшка уже отрендерилась.

Для этого, проще всего, использовать хук «view_layouts_base_html_head», он позволяет вставить содержимое в шапку страницы. Нам необходимо вставить ссылку на подключение js-файла с логикой вырезания или добавления определенных DOM-элементов. Что бы данный js-файл не подгружался на страницах, на которых он не нужен, его загрузку лучше загнать в условное выражение. Т.е. отсекать загрузку файла по экшину и контроллеру. Например:

  <% if controller_name == 'issues' && action_name == 'update' %>
    <%= javascript_include_tag :luxury_buttons_common, :plugin => :luxury_buttons %>
  <% end %>

В папке assets/javascript плагина должен находиться файл «luxury_buttons_common.js»:

jQuery(document).ready(function(){
//логика вырезания или добавления элементов на страницу
});

Иногда, более грамотно, встраивать строку подключения js-файла не через хук «view_layouts_base_html_head», а через определенный хук, который встраивает содержимое на ограниченном, нужном нам количестве страниц. Например, если нам нужно, что-то добавить или вырезать на странице задачи, то можно воспользоваться хуком «view_issues_form_details_bottom».

В таком случае, что бы файл подключался не в тело документа, а в шапку, нужно использовать конструкцию:

<% content_for :header_tags do %>
  <%= javascript_include_tag :luxury_buttons_common, :plugin => :luxury_buttons %>
<% end %>

Правда, с методом «content_for» в плагинах, от версии к версии возникают сложности.

Как изменять методы моделей, контроллеров и хелперов.

Изменение (патчинг) методов во многом похоже на изменение вьюшек и несет схожие проблемы.

Хуки в контроллерах и моделях

В контроллерах и моделях тоже встречаются хуки. Подключаются они иначе. В init.rb должна быть строчка которая подключает определенный хук. Например, хук, который вызывается перед сохранением новой задачи:

require 'luxury_buttons/controller_issues_new_before_save_hook'

В директории патчинга должен быть файл «controller_issues_new_before_save_hook.rb», например, с таким содержимым:

module LuxuryButtons
    class ControllerIssuesNewBeforeSaveHook < Redmine::Hook::ViewListener
   
      def controller_issues_new_before_save(context={})
        if context[:params] && context[:params][:issue]
          if (not context[:params][:issue][:assigned_to_id].nil?) and context[:params][:issue][:assigned_to_id].to_s==''
            context[:issue].assigned_to_id = context[:issue].author_id if context[:issue].new_record? and Setting.plugin_luxury_buttons['assign_to_author']
          end 
        end
        ''
      end
    end
end

Название модуля должно совпадать с названием плагина, название класса – с названием файла.

В данном случае, мы реализуем возможность автоматического назначения новой задачи на автора.

Патчинг методов

Как и во вьюшках, нужные хуки в Redmine есть далеко не всегда. И тогда нужно патчить методы модели, хелпера или контроллера.

Сперва, нужно подключить файл патчинга в init.rb. К примеру, нам нужно пропатчить метод «read_only_attribute_names» модели «Issue».

Rails.application.config.to_prepare do
  Issue.send(:include, LuxuryButtons::IssuePatch)
end

В папке патчинга должен быть файл «issue_patch.rb», примерно следующего содержания:

module LuxuryButtons
  module IssuePatch
    def self.included(base)
      base.extend(ClassMethods)
      base.send(:include, InstanceMethods)  
  
      base.class_eval do  
        alias_method_chain :read_only_attribute_names, :luxury_buttons
      end
    end
  
    module ClassMethods   
    end
  
    module InstanceMethods
      def read_only_attribute_names_with_luxury_buttons(user)
        attribute = read_only_attribute_names_without_luxury_buttons(user)
        if Setting.plugin_luxury_buttons['hidden_fields_into_new_issue_form'] && new_record?
          hidden_fields = Setting.plugin_luxury_buttons['hidden_fields_into_new_issue_form']
          attribute += hidden_fields
          attribute
        end
        attribute
      end
    end
  end
end

Конструкцией

alias_method_chain :read_only_attribute_names, :luxury_buttons

мы порождаем два метода «read_only_attribute_names_with_luxury_buttons» и «read_only_attribute_names_without_luxury_buttons».

Первый метод теперь будет вызываться вместо стандартного метода модели «read_only_attribute_names», второй метод является алиасом для стандартного метода «read_only_attribute_names».

Сочетанием двух методов можно патчить стандартный метод Redmine. В нашем примере, мы сперва вызываем стандартный метод Redmine, который возвращает массив значений, а затем, добавляем значения в этот массив.

Если в стандартном методе Redmine в новой версии что-то поменяется, то шансов, что наш патчинг будет работать корректно гораздо больше, чем если бы мы просто переписали стандартный метод Redmine добавив в него свою логику.

Важно! В Redmine наблюдаются какие-то проблемы с патчингом модели User. Для корректного патчинга нужно явно подключить следующие файлы:

require_dependency 'project'
require_dependency 'principal'
require_dependency 'user'

Статья не содержит всего, что хотелось бы сказать о написании плагинов под Redmine. Я попытался собрать основные методологии и подводные камни. Надеюсь статья будет полезна.

Автор: tdvsdv

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js