Про систему управления проектами Redmine наверное многие слышали, а некоторые возможно даже использовали в своей работе. Redmine — довольно гибкая кроссплатформенная система, написанная на известном фреймворке Ruby on Rails. Как и большинство подобных систем, Redmine позволяет расширять свою функциональность за счет сторонних плагинов. В данный момент уже имеется более тысячи таких плагинов на разный вкус и цвет. Я хочу рассказать об одном из них и о том как написать плагин к Redmine на его примере.
Repository plugin — плагин, позволяющий скачивать из репозитория (хранилища) проекта файлы и папки одним zip-архивом. Redmine предлагает наглядное руководство по созданию плагинов.
Итак, приступим к написанию плагина. Все, что написано ниже, касается redmine, установленного на веб сервер с ос Linux.
Для начала мы переходим в каталог, куда установлен redmine (например /var/www/redmine):
Установим переменную среды RAILS_ENV:
Теперь мы можем сгенерить необходимые для нашего плагина файлы:
Отредактируем файл vendor/plugins/redmine_repository/init.rb, указав
в нем имя, описание плагина, автора и минимальную версию redmine для которой написан плагин:
Наш плагин должен позволять выбирать какие файлы скачивать из хранилища. Поэтому в существующий вид хранилища надо добавить checkbox'ы
Т.к. надо изменить существующий вид хранилища, то самый простой способ — скопировать файлы, отвечающие за внешний вид хранилища в соответствующую директорию нашего плагина и затем отредактировать эти файлы:
Файл _dir_lsit.rhtml отвечат за отображение дерева файловой системы хранилища. Туда мы добавим кнопку «Download» для загрузки:
Для того, чтобы кнопка заработала, обернем весь код в form_tag:
Теперь добавим checkbox к каждой записи в отображении хранилища. Для этого в файл _dir_lsit_content.rhtml внесем следующие изменение:
вместо
напишем
Здесь используются две самописные функции на javascript:
Создадим их в новом файле assets/javascripts/repository.js
Подключим наш repository.js к нашему новому отображению хранилища (в файле show.rhtml):
Осталось добавить саму функцию архивированя выбранных папок и файлов. Реализация будет сделана на языке ruby. Дополнительно для поддержки zip архивов нужно поставить пакет rubyzip (gem install rubyzip).
Создадим класс RepositoryZip, который будет заниматься непосредственно архивированием (файл app/helpers/repository_zip.rb):
Теперь «пропатчим» существующий в redmine класс RepositoriesController, добавив к нему несколько методов (файл lib/repositories_controller_patch.rb). Не буду подробно останавливаться на логике работы этого «патча», просто приведу код:
Метод
Метод
Метод
Вот вообщем-то и все. Остатется только добавить разрешения на загрузку файлов пользователям системы redmine и локализацию плагина. Редактируем файл init.rb для добавления разрешений:
Файлы локализации находятся в каталоге config/locales. Для русского языка создаем файл ru.yml:
Перезапускаем redmine, настраиваем разрешения на скачивания файлов одним архивом для различных пользователей системы и пользуемся.
Repository plugin — плагин, позволяющий скачивать из репозитория (хранилища) проекта файлы и папки одним zip-архивом. Redmine предлагает наглядное руководство по созданию плагинов.
Итак, приступим к написанию плагина. Все, что написано ниже, касается redmine, установленного на веб сервер с ос Linux.
Для начала мы переходим в каталог, куда установлен redmine (например /var/www/redmine):
$ cd /var/www/redmine
Установим переменную среды RAILS_ENV:
$ export RAILS_ENV="production"
Теперь мы можем сгенерить необходимые для нашего плагина файлы:
$ ruby script/generate redmine_plugin repository
create vendor/plugins/redmine_repository/app/controllers
create vendor/plugins/redmine_repository/app/helpers
create vendor/plugins/redmine_repository/app/models
create vendor/plugins/redmine_repository/app/views
create vendor/plugins/redmine_repository/db/migrate
create vendor/plugins/redmine_repository/lib/tasks
create vendor/plugins/redmine_repository/assets/images
create vendor/plugins/redmine_repository/assets/javascripts
create vendor/plugins/redmine_repository/assets/stylesheets
create vendor/plugins/redmine_repository/lang
create vendor/plugins/redmine_repository/README
create vendor/plugins/redmine_repository/init.rb
create vendor/plugins/redmine_repository/lang/en.yml
Отредактируем файл vendor/plugins/redmine_repository/init.rb, указав
в нем имя, описание плагина, автора и минимальную версию redmine для которой написан плагин:
require 'redmine'
require 'dispatcher'
Redmine::Plugin.register :redmine_repository do
name 'Redmine Repository plugin'
author 'Sanny'
description 'This is a reposirory plugin for Redmine'
version '0.0.2'
requires_redmine :version_or_higher => '1.1.2'
end
Наш плагин должен позволять выбирать какие файлы скачивать из хранилища. Поэтому в существующий вид хранилища надо добавить checkbox'ы
Т.к. надо изменить существующий вид хранилища, то самый простой способ — скопировать файлы, отвечающие за внешний вид хранилища в соответствующую директорию нашего плагина и затем отредактировать эти файлы:
$ cp app/views/repositories/_dir_lsit.rhtml vendor/plugins/redmine_repository/app/views/repositories
$ cp app/views/repositories/_dir_lsit_content.rhtml vendor/plugins/redmine_repository/app/views/repositories
$ cp app/views/repositories/show.rhtml vendor/plugins/redmine_repository/app/views/repositories
Файл _dir_lsit.rhtml отвечат за отображение дерева файловой системы хранилища. Туда мы добавим кнопку «Download» для загрузки:
<% if authorize_for('repositories', 'entries_operation') %>
<div style="float: right;">
<%= submit_tag(l(:Download), :name => "download_entries") %>
</div>
<% end %>
Для того, чтобы кнопка заработала, обернем весь код в form_tag:
<% form_tag({:action => "entries_operation"}, :method => :post, :id => "Entries") do %>
...
<% if authorize_for('repositories', 'entries_operation') %>
<div style="float: right;">
<%= submit_tag(l(:Download), :name => "download_entries") %>
</div>
<% end %>
<p> </p>
<% end %>
Теперь добавим checkbox к каждой записи в отображении хранилища. Для этого в файл _dir_lsit_content.rhtml внесем следующие изменение:
вместо
<% if entry.is_dir? %>
<span class="expander" onclick="<%= remote_function :url => {:action => 'show', :id => @project, :path => to_path_param(ent_path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
:method => :get,
:update => { :success => tr_id },
:position => :after,
:success => "scmEntryLoaded('#{tr_id}')",
:condition => "scmEntryClick('#{tr_id}')"%>"> </span>
<% end %>
напишем
<% if entry.is_dir? %>
<span class="expander" onclick="<%= remote_function :url => {:action => 'show', :id => @project, :path => to_path_param(entry.path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id, :p
arent_val => entry.path},
:method => :get,
:update => { :success => tr_id },
:position => :after,
:success => "scmEntryLoaded('#{tr_id}')",
:complete => "checkBranch('#{tr_id}', getParentNodeChecked('#{params[:parent_val]}'))",
:condition => "scmEntryClick('#{tr_id}')"%>"> </span>
<span><%= check_box_tag("folders[]", entry.path, false, :id => params[:parent_id], :onclick => "checkBranch('#{tr_id}', this.checked);" ) if authorize_for('repositories', 'entries_operation')
%></span>
<% else %>
<span style="padding-left: 8px"> </span>
<span><%= check_box_tag("files[]", entry.path, false, :id => params[:parent_id]) if authorize_for('repositories', 'entries_operation') %></span>
<% end %>
Здесь используются две самописные функции на javascript:
getParentNodeChecked(parentVal)
— определяет состояние checkbox'а родителя данного узла дерева,checkBranch(parentId, checked)
— устанавливает checkbox'ы потомков узла дерева в состояние как у родителя (грубо говоря, если выделен каталог, то выделются все подкаталоги и файлы в этом каталоге).Создадим их в новом файле assets/javascripts/repository.js
function getParentNodeChecked(parentVal)
{
var form = document.getElementById('Entries');
for (var i=0;i<form.elements.length;i++)
{
var e = form.elements[i];
if(e.type=='checkbox' && e.value == parentVal)
return e.checked;
}
}
function checkBranch(parentId, checked)
{
var allchecked = true;
var has_check_tree = false;
var form = document.getElementById('Entries');
for (var i=0;i<form.elements.length;i++)
{
var e = form.elements[i];
if(e.type=='checkbox')
{
if(e.id == parentId)
{
e.checked = !checked;
e.click();
}
allchecked = allchecked && e.checked;
if(e.name == "check_tree") has_check_tree = true;
}
}
if(has_check_tree)
form.check_tree.checked = allchecked;
}
Подключим наш repository.js к нашему новому отображению хранилища (в файле show.rhtml):
...
<% content_for :header_tags do %>
<%= stylesheet_link_tag "scm" %>
<%= javascript_include_tag "repository.js", :plugin => "redmine_repository" %>
<% end %>
Осталось добавить саму функцию архивированя выбранных папок и файлов. Реализация будет сделана на языке ruby. Дополнительно для поддержки zip архивов нужно поставить пакет rubyzip (gem install rubyzip).
Создадим класс RepositoryZip, который будет заниматься непосредственно архивированием (файл app/helpers/repository_zip.rb):
require 'zip/zip'
require 'zip/zipfilesystem'
class RepositoryZip
attr_reader :file_count
def initialize()
@zip = Tempfile.new(["repository_zip",".zip"])
@zip_file = Zip::ZipOutputStream.new(@zip.path)
@file_count = 0
end
def finish
@zip_file.close unless @zip_file.nil?
@zip.path unless @zip.nil?
end
def close
@zip_file.close unless @zip_file.nil?
@zip.close unless @zip.nil?
end
def add_file(file, cat)
@zip_file.put_next_entry(file)
@zip_file.write(cat)
@file_count += 1
end
def add_folder(folder)
@zip_file.put_next_entry(folder + "/")
end
end
Теперь «пропатчим» существующий в redmine класс RepositoriesController, добавив к нему несколько методов (файл lib/repositories_controller_patch.rb). Не буду подробно останавливаться на логике работы этого «патча», просто приведу код:
require 'tree'
require_dependency 'application_controller'
require_dependency 'repositories_controller'
require_dependency 'repository_zip'
module RepositoriesControllerPatch
def self.included(base) # :nodoc:
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
base.class_eval do
unloadable # послать unloadable чтобы не перегружать при разработке
end
end
module ClassMethods
end
module InstanceMethods
def entries_operation
selected_folders = params[:folders].nil? ? [] : params[:folders]
selected_files = params[:files].nil? ? [] : params[:files]
if selected_folders.empty? && selected_files.empty?
flash[:warning] = l(:warning_no_entries_selected)
redirect_to :action => "show", :id => @project, :path => @path
return
end
# make a selected files and folders tree
selected_tree = Tree::TreeNode.new(".", "root")
selected_files.each do |file|
folder = Pathname.new(file).dirname.to_s
selected_tree_node = selected_tree
if !folder.match(/^\.+$/)
folder.split("/").each do
if selected_tree_node[folder].nil?
selected_tree_node = selected_tree_node.add(Tree::TreeNode.new(folder, "folder"))
else
selected_tree_node = selected_tree_node[folder]
end
end
selected_tree_node << Tree::TreeNode.new(file, "file")
else
selected_tree << Tree::TreeNode.new(file, "file")
end
end
selected_folders.each do |folder|
selected_tree_node = selected_tree
folder.split("/").each do
if selected_tree_node[folder].nil?
selected_tree_node = selected_tree_node.add(Tree::TreeNode.new(folder, "folder"))
else
selected_tree_node = selected_tree_node[folder]
end
end
selected_tree_node << Tree::TreeNode.new(file, "file")
else
selected_tree << Tree::TreeNode.new(file, "file")
end
end
selected_folders.each do |folder|
selected_tree_node = selected_tree
folder.split("/").each do
if selected_tree_node[folder].nil?
selected_tree_node = selected_tree_node.add(Tree::TreeNode.new(folder, "folder"))
else
selected_tree_node = selected_tree_node[folder]
end
end
end
begin
if !params[:email_entries].blank?
email_entries(selected_tree)
else
download_entries(selected_tree)
end
rescue => e
flash[:warning] = l(:error_in_getting_files) + " (" + e.message + ")"
redirect_to :action => "show", :id => @project
end
end
def download_entries(selected_tree)
zip = RepositoryZip.new
zip_entries(zip, selected_tree)
send_file(zip.finish,
:filename => filename_for_content_disposition(@project.name + "-" + DateTime.now.strftime("%y%m%d%H%M%S") + ".zip"),
:type => "application/zip",
:disposition => "attachment")
ensure
zip.close unless zip.nil?
end
def zip_entries(zip, selected_tree)
selected_tree.children.each do |node|
if node.content == "file"
zip.add_file(node.name, @repository.cat(node.name, @rev))
else
zip.add_folder(node.name)
if node.hasChildren?
# add selected subfolders
zip_entries(zip, node)
else
# add all subfolders with files
entries = @repository.entries(node.name, @rev)
entries.each do |entry|
node << Tree::TreeNode.new(entry.path, entry.is_dir? ? "folder" : "file")
end
end
zip_entries(zip, node)
end
end
zip
end
end # of InstaceMethods
end # of module
RepositoriesController.send(:include, RepositoriesControllerPatch)
Метод
entries_operation
отвечает за формирования списка файлов и папок для архивации.Метод
download_entries
отвечает за загрузку архива пользователем.Метод
zip_entries
отвечает за архивирования выбранных файлоа и папок.Вот вообщем-то и все. Остатется только добавить разрешения на загрузку файлов пользователям системы redmine и локализацию плагина. Редактируем файл init.rb для добавления разрешений:
require 'redmine'
require 'dispatcher'
require 'repositories_controller_patch'
Redmine::Plugin.register :redmine_repository do
name 'Redmine Repository plugin'
author 'Sanny'
description 'This is a reposirory plugin for Redmine'
version '0.0.2'
requires_redmine :version_or_higher => '1.1.2'
end
Redmine::AccessControl.map do |map|
map.project_module :repository do |map|
map.permission :operations, :repositories => [:entries_operation]
end
end
Файлы локализации находятся в каталоге config/locales. Для русского языка создаем файл ru.yml:
ru:
Download: "Download"
warning_no_entries_selected: "Ничего не выбрано"
error_in_getting_files: "Ошибка при доступе к файлам"
permission_operations: "Групповая загрузка файлов одним архивом"
Перезапускаем redmine, настраиваем разрешения на скачивания файлов одним архивом для различных пользователей системы и пользуемся.