- PVSM.RU - https://www.pvsm.ru -
В своем прошлом посте [1] я постарался достаточно детально описать все тонкости установки Redmine на Linux Ubuntu. В этом, хочу рассказать о тонкостях написания плагинов под Redmine, об основных возможностях изменения функциональности стандартного Redmine, о подводных камнях, которые встречались моей команде на этом пути.
Думаю, эта статья будет полезна тем, кто уже знаком с основами фреймворка Ruby on Rails и хочет начать разрабатывать плагины для Redmine.
Прежде всего, стоит разделить все плагины Redmine на две категории:
В первую попадают те плагины, которые фактически не затрагивают функциональность стандартного Redmine. По сути это обычные Rails-приложения внутри Redmine, с ними возникает мало сложностей, поэтому они малоинтересны. На официальном сайте Redmine есть неплохой туториал, подробно описывающий как создать плагин для голосования [2].
Все немного сложнее, когда плагин должен изменять встроенную функциональность!
Начнем с команды, которая создает структуру папок для плагина Redmine. Пусть наш плагин будет называться Luxury Buttons. Перейдем в корневую папку Redmine, запустим команду, создающую структуру папок:
$cd /usr/share/srv-redmine/redmine-2.3
$rails generate redmine_plugin LuxuryButtons
После выполнения команды в папке plugins должна появиться папка luxury_buttons со следующей структурой:
В папку lib стоит сразу добавить, папку, совпадающую с названием плагина, т.е. папку luxury_buttons (далее папка патчинга). В этой папке, в дальнейшем, будут лежать файлы патчинга различных методов Redmine.
Почему мы назвали эту папку также как назвали плагин? Это просто рекомендация, папку можно назвать и по-другому, но тут возникает первый подводный камень: если в другом плагине название этой папки будет совпадать, и будет совпадать название файла патчинга, то один из файлов патчинга просто не примениться! Поэтому, я рекомендую, называть папку патчинга одноименно с названием плагина. Такой способ минимизирует возникновение ошибок!
Допустим нам нужно что-то добавить в стандартную вьюшку Redmine. Самый простой и самый неправильный способ сделать это – переписать вьюшку внутри плагина. Обычно это делается путем копирования файла-вьюшки из ядра Redmine в соответствующую директорию плагина и дальнейшим редактированием этого файла. Вот, например, в одном из наших плагинов, мы переписываем вьюшку с формой сохранения запроса.
Почему так делать плохо:
Поэтому, лучше использовать альтернативные методы.
Хук во вьюшке – это такая строчка кода, которая позволяет встроить во вьюшку свое содержимое. Чтобы найти хук, нужно просто выполнить поиск подстроки «hook» по всем файлам Redmine или можно воспользоваться вот этой табличкой [3].
Мы стараемся хранить все подключения хуков вьюшек в одном файле. Этот файл нужно подключить в 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» в плагинах, от версии к версии возникают сложности [4].
Изменение (патчинг) методов во многом похоже на изменение вьюшек и несет схожие проблемы.
В контроллерах и моделях тоже встречаются хуки. Подключаются они иначе. В 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 [5]. Для корректного патчинга нужно явно подключить следующие файлы:
require_dependency 'project'
require_dependency 'principal'
require_dependency 'user'
Статья не содержит всего, что хотелось бы сказать о написании плагинов под Redmine. Я попытался собрать основные методологии и подводные камни. Надеюсь статья будет полезна.
Автор: tdvsdv
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ruby/47657
Ссылки в тексте:
[1] своем прошлом посте: http://habrahabr.ru/company/monandco/blog/198496/
[2] неплохой туториал, подробно описывающий как создать плагин для голосования: http://www.redmine.org/projects/redmine/wiki/Plugin_Tutorial
[3] вот этой табличкой: http://www.redmine.org/projects/redmine/wiki/Hooks_List
[4] возникают сложности: http://www.redmine.org/issues/11105
[5] какие-то проблемы с патчингом модели User: http://www.redmine.org/issues/11035
[6] Источник: http://habrahabr.ru/post/201064/
Нажмите здесь для печати.