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

    В своем прошлом посте я постарался достаточно детально описать все тонкости установки 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 со следующей структурой:

    В папку lib стоит сразу добавить, папку, совпадающую с названием плагина, т.е. папку luxury_buttons (далее папка патчинга). В этой папке, в дальнейшем, будут лежать файлы патчинга различных методов 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. Я попытался собрать основные методологии и подводные камни. Надеюсь статья будет полезна.
    • +15
    • 16,4k
    • 4

    Монастырёв и Ко

    43,00

    Компания

    Поделиться публикацией
    Комментарии 4
      0
      как можно переопределить встроенный роут?
        0
        Насколько я понимаю. Никак. А зачем вам это нужно? Мы написали более 20 плагинов и не разу не пригодилось.
          0
          Нужно было прицепить Microsoft Office Web Apps к Redmine. Т.е. по клику на вложение (если это офисный документ) он открывается в Web Apps, вместо скачивания. Т.е. фактически надо переопределить ссылку у вложения. Навскидку пришло в голову 2 идеи: 1) делать редирект на веб-сервере; 2) переопределить экшен скачивания аттачмента. Но в этом экшене никаких хуков нет вроде как. А вот если переопределить роут на свой контроллер, можно проверить расширение и редиректить на Web Apps.
        0
        переопределить экшен скачивания аттачмента.


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

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

        Самое читаемое